aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.clang-format2
-rw-r--r--.gitignore10
-rw-r--r--README.md180
-rw-r--r--TODO127
-rwxr-xr-xbuild.sh57
-rwxr-xr-xdebug-install.sh20
-rw-r--r--external/NvFBC.h (renamed from include/NvFBC.h)0
-rw-r--r--extra/gpu-screen-recorder.env9
-rw-r--r--extra/gpu-screen-recorder.service23
-rw-r--r--extra/gsr-nvidia.conf1
-rwxr-xr-xextra/install_preserve_video_memory.sh8
-rw-r--r--include/NvFBCLibrary.hpp246
-rw-r--r--include/capture/capture.h68
-rw-r--r--include/capture/kms.h50
-rw-r--r--include/capture/kms_cuda.h20
-rw-r--r--include/capture/kms_vaapi.h20
-rw-r--r--include/capture/nvfbc.h22
-rw-r--r--include/capture/xcomposite.h51
-rw-r--r--include/capture/xcomposite_cuda.h14
-rw-r--r--include/capture/xcomposite_vaapi.h13
-rw-r--r--include/color_conversion.h58
-rw-r--r--include/cuda.h108
-rw-r--r--include/cursor.h26
-rw-r--r--include/defs.h28
-rw-r--r--include/egl.h285
-rw-r--r--include/library_loader.h17
-rw-r--r--include/overclock.h17
-rw-r--r--include/shader.h19
-rw-r--r--include/sound.hpp26
-rw-r--r--include/utils.h46
-rw-r--r--include/vec2.h12
-rw-r--r--include/window_texture.h27
-rw-r--r--include/xnvctrl.h45
-rwxr-xr-xinstall.sh22
-rwxr-xr-xinstall_ubuntu.sh27
-rw-r--r--kms/client/kms_client.c426
-rw-r--r--kms/client/kms_client.h24
-rw-r--r--kms/kms_shared.h60
-rw-r--r--kms/server/.gitignore1
-rw-r--r--kms/server/kms_server.c572
-rw-r--r--kms/server/project.conf11
-rw-r--r--project.conf27
-rwxr-xr-xscripts/interactive.sh (renamed from interactive.sh)2
-rwxr-xr-xscripts/list-sinks.sh (renamed from list-sinks.sh)0
-rwxr-xr-xscripts/record-application-name.sh6
-rwxr-xr-xscripts/replay-application-name.sh6
-rwxr-xr-xscripts/replay.sh (renamed from replay.sh)2
-rwxr-xr-xscripts/save-replay.sh4
-rwxr-xr-xscripts/start-replay.sh5
-rwxr-xr-xscripts/stop-replay.sh3
-rwxr-xr-xscripts/toggle-recording-selected.sh (renamed from toggle-recording-selected.sh)2
-rwxr-xr-xscripts/twitch-stream-local-copy.sh (renamed from twitch-stream-local-copy.sh)2
-rwxr-xr-xscripts/twitch-stream.sh (renamed from twitch-stream.sh)2
-rwxr-xr-xscripts/youtube-hls-stream.sh11
-rw-r--r--src/capture/capture.c400
-rw-r--r--src/capture/kms.c369
-rw-r--r--src/capture/kms_cuda.c181
-rw-r--r--src/capture/kms_vaapi.c135
-rw-r--r--src/capture/nvfbc.c535
-rw-r--r--src/capture/xcomposite.c299
-rw-r--r--src/capture/xcomposite_cuda.c155
-rw-r--r--src/capture/xcomposite_vaapi.c109
-rw-r--r--src/color_conversion.c469
-rw-r--r--src/cuda.c117
-rw-r--r--src/cursor.c127
-rw-r--r--src/egl.c651
-rw-r--r--src/library_loader.c34
-rw-r--r--src/main.cpp3212
-rw-r--r--src/overclock.c281
-rw-r--r--src/shader.c143
-rw-r--r--src/sound.cpp455
-rw-r--r--src/utils.c450
-rw-r--r--src/window_texture.c123
-rw-r--r--src/xnvctrl.c46
-rw-r--r--study/color_space_transform_matrix.pngbin0 -> 7166 bytes
-rwxr-xr-xstudy/create_matrix.py48
-rwxr-xr-xuninstall.sh9
77 files changed, 9765 insertions, 1453 deletions
diff --git a/.clang-format b/.clang-format
deleted file mode 100644
index 80d3293..0000000
--- a/.clang-format
+++ /dev/null
@@ -1,2 +0,0 @@
-BasedOnStyle: LLVM
-IndentWidth: 4
diff --git a/.gitignore b/.gitignore
index 7d676bc..aa01f09 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,18 @@ compile_commands.json
tests/sibs-build/
tests/compile_commands.json
+external/wlr-export-dmabuf-unstable-v1-client-protocol.h
+external/wlr-export-dmabuf-unstable-v1-protocol.c
+
.clangd/
.cache/
+.vscode/
*.o
gpu-screen-recorder
+gsr-kms-server
+
+*.mp4
+*.flv
+*.mkv
+*.mov
diff --git a/README.md b/README.md
index 9eca5ad..7882004 100644
--- a/README.md
+++ b/README.md
@@ -1,50 +1,174 @@
-# gpu screen recorder
-This is a screen recorder that has minimal impact on system performance by recording a window using the GPU only,
+![](https://dec05eba.com/images/gpu_screen_recorder_logo_small.png)
+
+# GPU Screen Recorder
+This is a screen recorder that has minimal impact on system performance by recording your monitor using the GPU only,
similar to shadowplay on windows. This is the fastest screen recording tool for Linux.
-This screen recorder can be used for recording your desktop offline, for live streaming and for nvidia-like instant replay,
-where only the last few seconds are saved.
+This screen recorder can be used for recording your desktop offline, for live streaming and for nvidia shadowplay-like instant replay,
+where only the last few minutes are saved.
-## Note
-Might now work when using a compositor such as picom when using the glx backend. Using the xrender backend with picom fixes this issue.\
-Does not work when using gtk client side decorations (such as on Pop OS). Either disable those (if possible), install gtk-nocsd or record the whole monitor/screen if you have NvFBC.\
-NvFBC doesn't work with PRIME, so if you are using PRIME then you can't record the monitor/screen, you have to record a single window.\
-If you are using a variable refresh rate monitor, then choose to record "screen-direct". This will allow variable refresh rate to work when recording fullscreen applications. Note that some applications such as mpv will not work in fullscreen mode. A fix is being developed for this.
+Supported video codecs:
+* H264 (default on Intel)
+* HEVC (default on AMD and NVIDIA)
+* AV1 (not currently supported on NVIDIA if you use GPU Screen Recorder flatpak)
+Supported audio codecs:
+* Opus (default)
+* AAC
+* FLAC
+
+## Note
+This software works with x11 and wayland, but when using Wayland then only monitors can be recorded.
+### TEMPORARY ISSUES
+1) screen-direct capture has been temporary disabled as it causes issues with stuttering. This might be a nvfbc bug.
+2) Videos are in variable framerate format. Use MPV to play such videos, otherwise you might experience stuttering in the video if you are using a buggy video player. You can try saving the video into a .mkv file instead as some software may have better support for .mkv files (such as kdenlive). You can use the "-fm cfr" option to to use constant framerate mode.
+3) HDR capture is supported (on wayland), but all GPU drivers have bugs that ignore HDR metadata so the HDR metadata will be missing in the video file. I will eventually patch the video file to workaround these GPU driver issues.
+4) Opus and FLAC audio codecs are disabled at the moment because of temporary issues.
+### AMD/Intel/Wayland root permission
+When recording a window under AMD/Intel no special user permission is required, however when recording a monitor (or when using wayland) the program needs root permission (to access KMS).\
+To make this safer, the part that needs root access has been moved to its own executable (to make it as small as possible).\
+For you as a user this only means that if you installed GPU Screen Recorder as a flatpak then a prompt asking for root password will show up when you start recording.
# Performance
+On a system with a i5 4690k CPU and a GTX 1080 GPU:\
When recording Legend of Zelda Breath of the Wild at 4k, fps drops from 30 to 7 when using OBS Studio + nvenc, however when using this screen recorder the fps remains at 30.\
-When recording GTA V at 4k on highest settings, fps drops from 60 to 23 when using obs-nvfbc + nvenc, however when using this screen recorder the fps only drops to 55. The quality is also much better when using gpu-screen-recorder.\
-It is recommended to save the video to a SSD because of the large file size, which a slow HDD might not be fast enough to handle.\
-Using NvFBC (recording the monitor/screen) is not faster than not using NvFBC (recording a single window) with gpu screen recorder, in fact it might be a tiny bit slower.
+When recording GTA V at 4k on highest settings, fps drops from 60 to 23 when using obs-nvfbc + nvenc, however when using this screen recorder the fps only drops to 58. The quality is also much better when using gpu screen recorder.\
+GPU Screen Recorder also produces much smoother videos than OBS when GPU utilization is close to 100%, see comparison here: [https://www.youtube.com/watch?v=zfj4sNVLLLg](https://www.youtube.com/watch?v=zfj4sNVLLLg).\
+It is recommended to save the video to a SSD because of the large file size, which a slow HDD might not be fast enough to handle. Using variable framerate mode (-fm vfr) which is the default is also recommended as this reduces encoding load. Ultra quality is also overkill most of the time, very high (the default) or lower quality is usually enough.
+## Note about optimal performance on NVIDIA
+NVIDIA driver has a "feature" (read: bug) where it will downclock memory transfer rate when a program uses cuda (or nvenc, which uses cuda), such as GPU Screen Recorder. To work around this bug, GPU Screen Recorder can overclock your GPU memory transfer rate to it's normal optimal level.\
+To enable overclocking for optimal performance use the `-oc` option when running GPU Screen Recorder. You also need to have "Coolbits" NVIDIA X setting set to "12" to enable overclocking. You can automatically add this option if you run `sudo nvidia-xconfig --cool-bits=12` and then reboot your computer.\
+Note that this only works when Xorg server is running as root, and using this option will only give you a performance boost if the game you are recording is bottlenecked by your GPU.\
+Note! use at your own risk!
+
# Installation
If you are running an Arch Linux based distro, then you can find gpu screen recorder on aur under the name gpu-screen-recorder-git (`yay -S gpu-screen-recorder-git`).\
-If you are running an Ubuntu based distro then run `install_ubuntu.sh` as root: `sudo ./install_ubuntu.sh`.\
+If you are running another distro then you can run `sudo ./install.sh`, but you need to manually install the dependencies, as described below.\
+You can also install gpu screen recorder ([the gtk gui version](https://git.dec05eba.com/gpu-screen-recorder-gtk/)) from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder), which is the easiest method
+to install GPU Screen Recorder on non-arch based distros.\
+The only official ways to install GPU Screen Recorder is either from source, AUR or flathub. If you install GPU Screen Recorder from somewhere else and have an issue then try installing it
+from one of the official sources before reporting it as an issue.
+If you install GPU Screen Recorder flatpak, which is the gtk gui version then you can still run GPU Screen Recorder command line by using the flatpak command option, for example `flatpak run --command=gpu-screen-recorder com.dec05eba.gpu_screen_recorder -w screen -f 60 -o video.mp4`. Note that if you want to record your monitor on AMD/Intel then you need to install the flatpak system-wide (like so: `flatpak install flathub --system com.dec05eba.gpu_screen_recorder`).
-On other distros you need to install dependencies manually and run `build.sh`. Dependencies: `glew glfw3 cuda ffmpeg libx11 libxcomposite libpulse-simple`.\
-Recording monitors requires a gpu with NvFBC support (note: this is not required when recording a single window!). Normally only tesla and quadro gpus support this, but by using [nvidia-patch](https://github.com/keylase/nvidia-patch) or [nvlax](https://github.com/illnyang/nvlax) you can do this on all gpus that support nvenc as well (gpus as old as the nvidia 600 series), provided you are not using outdated gpu drivers.
+# Dependencies
+## AMD
+libglvnd (which provides libgl and libegl)\
+mesa\
+ffmpeg (libavcodec, libavformat, libavutil, libswresample, libavfilter)\
+x11 (libx11, libxcomposite, libxrandr, xfixes)\
+libpulse\
+vaapi (libva, libva-mesa-driver)\
+libdrm\
+libcap\
+wayland-client
+## Intel
+libglvnd (which provides libgl and libegl)\
+mesa\
+ffmpeg (libavcodec, libavformat, libavutil, libswresample, libavfilter)\
+x11 (libx11, libxcomposite, libxrandr, xfixes)\
+libpulse\
+vaapi (libva, intel-media-driver/libva-intel-driver)\
+libdrm\
+libcap\
+wayland-client
+## NVIDIA
+libglvnd (which provides libgl and libegl)\
+ffmpeg (libavcodec, libavformat, libavutil, libswresample, libavfilter)\
+x11 (libx11, libxcomposite, libxrandr, xfixes)\
+libpulse\
+cuda runtime (libcuda.so.1) (libnvidia-compute)\
+nvenc (libnvidia-encode)\
+libva\
+libdrm\
+libcap\
+wayland-client\
+nvfbc (libnvidia-fbc1, when recording the screen on x11)\
+xnvctrl (libxnvctrl0, when using the `-oc` option)
# How to use
-Run `interactive.sh` or run gpu-screen-recorder directly, for example: `gpu-screen-recorder -w $(xdotool selectwindow) -c mp4 -f 60 -a "$(pactl get-default-sink).monitor" -o test_video.mp4`\
-Then stop the screen recorder with Ctrl+C, which will also save the recording.\
-Send signal SIGUSR1 (`killall -SIGUSR1 gpu-screen-recorder`) to gpu-screen-recorder when in replay mode to save the replay. The paths to the saved files is output to stdout after the recording is saved.\
-There is also a gui for the gpu-screen-recorder called [gpu-screen-recorder-gtk](https://git.dec05eba.com/gpu-screen-recorder-gtk/).
+Run `gpu-screen-recorder --help` to see all options.
+## Recording
+Here is an example of how to record all monitors and the default audio output: `gpu-screen-recorder -w screen -f 60 -a "$(pactl get-default-sink).monitor" -o ~/Videos/test_video.mp4` then stop the screen recorder with `Ctrl+C`, which will also save the recording. You can record a single monitor if you change `-w screen` to the name of a monitor, which you can find if you run the `xrandr`. An example of a monitor name is HDMI-1.
+## Streaming
+Streaming works the same as recording, but the `-o` argument should be path to the live streaming service you want to use (including your live streaming key). Take a look at scripts/twitch-stream.sh to see an example of how to stream to twitch.
+## Replay mode
+Run `gpu-screen-recorder` with the `-c mp4` and `-r` option, for example: `gpu-screen-recorder -w screen -f 60 -r 30 -c mp4 -o ~/Videos`. Note that in this case, `-o` should point to a directory.\
+If `-mf yes` is set, replays are save in folders based on the date.
+To save a video in replay mode, you need to send signal SIGUSR1 to gpu screen recorder. You can do this by running `killall -SIGUSR1 gpu-screen-recorder`.\
+To stop recording send SIGINT to gpu screen recorder. You can do this by running `killall -SIGINT gpu-screen-recorder` or pressing `Ctrl-C` in the terminal that runs gpu screen recorder.\
+To pause/unpause recording send SIGUSR2 to gpu screen recorder. You can do this by running `killall -SIGUSR2 gpu-screen-recorder`. This is only applicable and useful when recording (not streaming nor replay).\
+The file path to the saved replay is output to stdout. All other output from GPU Screen Recorder is output to stderr.\
+The replay buffer is stored in ram (as encoded video), so don't use a too large replay time and/or video quality unless you have enough ram to store it.
+## Finding audio device name
+You can find the default output audio device (headset, speakers (in other words, desktop audio)) with the command `pactl get-default-sink`. Add `monitor` to the end of that to use that as an audio input in gpu screen recorder.\
+You can find the default input audio device (microphone) with the command `pactl get-default-source`. This input should not have `monitor` added to the end when used in gpu screen recorder.\
+Example of recording both desktop audio and microphone: `gpu-screen-recorder -w screen -f 60 -a "$(pactl get-default-sink).monitor" -a "$(pactl get-default-source)" -o ~/Videos/test_video.mp4`.\
+A name (that is visible to pipewire) can be given to an audio input device by prefixing the audio input with `<name>/`, for example `dummy/$(pactl get-default-sink).monitor`.\
+Note that if you use multiple audio inputs then they are each recorded into separate audio tracks in the video file. If you want to merge multiple audio inputs into one audio track then separate the audio inputs by "|" in one -a argument,
+for example `-a "$(pactl get-default-sink).monitor|$(pactl get-default-source)"`.
+
+There is also a gui for the gpu screen recorder called [gpu-screen-recorder-gtk](https://git.dec05eba.com/gpu-screen-recorder-gtk/).
+## Simple way to run replay without gui
+Run the script `scripts/start-replay.sh` to start replay and then `scripts/save-replay.sh` to save a replay and `scripts/stop-replay.sh` to stop the replay. The videos are saved to `$HOME/Videos`.
+You can use these scripts to start replay at system startup if you add `scripts/start-replay.sh` to startup (this can be done differently depending on your desktop environment / window manager) and then go into
+hotkey settings on your system and choose a hotkey to run the script `scripts/save-replay.sh`. Modify `scripts/start-replay.sh` if you want to use other replay options.
+## Run replay on system startup
+If you installed GPU Screen Recorder from AUR or from source and you are running a distro that uses systemd then you will have a systemd service installed that can be started with `systemctl enable --now --user gpu-screen-recorder`. This systemd service runs GPU Screen Recorder on system startup.\
+It's configured with `$HOME/.config/gpu-screen-recorder.env` (create it if it doesn't exist). You can look at [extra/gpu-screen-recorder.env](https://git.dec05eba.com/gpu-screen-recorder/plain/extra/gpu-screen-recorder.env) to see an example.
+You can see which variables that you can use in the `gpu-screen-recorder.env` file by looking at the `extra/gpu-screen-recorder.service` file. Note that all of the variables are optional, you only have to set the ones that are you interested in.
+You can use the `scripts/save-replay.sh` script to save a replay and by default the systemd service saves videos in `$HOME/Videos`.\
+If you are using a NVIDIA GPU then it's recommended to set PreserveVideoMemoryAllocations=1 as mentioned in the section below.
+## Examples
+Look at the [scripts](https://git.dec05eba.com/gpu-screen-recorder/tree/scripts) directory for script examples. For example if you want to automatically save a recording/replay into a folder with the same name as the game you are recording.
+# Issues
+## NVIDIA
+Nvidia drivers have an issue where CUDA breaks if CUDA is running when suspend/hibernation happens, and it remains broken until you reload the nvidia driver. To fix this, either disable suspend or tell the NVIDIA driver to preserve video memory on suspend/hibernate by using the `NVreg_PreserveVideoMemoryAllocations=1` option. You can run `sudo extra/install_preserve_video_memory.sh` to automatically add that option to your system.
+
+# Reporting bugs
+Issues are reported on this Github page: [https://github.com/dec05eba/gpu-screen-recorder-issues/issues](https://github.com/dec05eba/gpu-screen-recorder-issues/issues)
+# Contributing patches
+See [https://git.dec05eba.com/?p=about](https://git.dec05eba.com/?p=about)
# Demo
[![Click here to watch a demo video on youtube](https://img.youtube.com/vi/n5tm0g01n6A/0.jpg)](https://www.youtube.com/watch?v=n5tm0g01n6A)
# FAQ
## How is this different from using OBS with nvenc?
-OBS only uses the gpu for video encoding, but the window image that is encoded is sent from the GPU to the CPU and then back to the GPU. These operations are very slow and causes all of the fps drops when using OBS. OBS only uses the GPU efficiently on Windows 10 and Nvidia.\
-This gpu-screen-recorder keeps the window image on the GPU and sends it directly to the video encoding unit on the GPU by using CUDA. This means that CPU usage remains at around 0% when using this screen recorder.
+OBS only uses the gpu for video encoding, but the window image that is encoded is copied from the GPU to the CPU and then back to the GPU (video encoding unit). These operations are very slow and causes all of the fps drops when using OBS. OBS only uses the GPU efficiently on Windows 10 and Nvidia.\
+This gpu screen recorder keeps the window image on the GPU and sends it directly to the video encoding unit on the GPU by using CUDA. This means that CPU usage remains at around 0% when using this screen recorder.
+## How is this different from using OBS NvFBC plugin?
+The plugin does everything on the GPU and gives the texture to OBS, but OBS does not know how to use the texture directly on the GPU so it copies the texture to the CPU and then back to the GPU (video encoding unit). These operations are very slow and causes a lot of fps drops unless you have a fast CPU. This is especially noticable when recording at higher resolutions than 1080p.
## How is this different from using FFMPEG with x11grab and nvenc?
FFMPEG only uses the GPU with CUDA when doing transcoding from an input video to an output video, and not when recording the screen when using x11grab. So FFMPEG has the same fps drop issues that OBS has.
+## It tells me that my AMD/Intel GPU is not supported or that my GPU doesn't support h264/hevc, but that's not true!
+Some linux distros (such as manjaro and fedora) disable hardware accelerated h264/hevc on AMD/Intel because of "patent license issues". If you are using an arch-based distro then you can install mesa-git instead of mesa and if you are using another distro then you may have to switch to a better distro. On fedora based distros you can follow this: [Hardware Accelerated Codec](https://rpmfusion.org/Howto/Multimedia).\
+If you installed GPU Screen Recorder flatpak then you can try installing mesa-extra freedesktop runtime by running this command: `flatpak install --system org.freedesktop.Platform.GL.default//23.08-extra`
+## I have an old nvidia GPU that supports nvenc but I get a cuda error when trying to record
+Newer ffmpeg versions don't support older nvidia cards. Try installing GPU Screen Recorder flatpak from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder) instead. It comes with an older ffmpeg version which might work for your GPU.
+## I get a black screen/glitches while live streaming
+It seems like ffmpeg earlier than version 6.1 has some type of bug. Install ffmpeg 6.1 and then reinstall GPU Screen Recorder to fix this issue. The flatpak version of GPU Screen Recorder comes with ffmpeg 6.1 so no extra steps are needed.
+## I can't play the video in my browser directly or in discord
+Browsers and discord don't support hevc video codec at the moment. Choose h264 video codec instead with the -k h264 option.
+Note that websites such as youtube support hevc so there is no need to choose h264 video codec if you intend to upload the video to youtube or if you want to play the video locally or if you intend to
+edit the video with a video editor. Hevc allows for better video quality (especially at lower file sizes) so hevc (or av1) is recommended for source videos.
+## I get a black bar on the right/bottom in the video
+This is mostly an issue on AMD. For av1 it's a hardware issue, see: https://gitlab.freedesktop.org/mesa/mesa/-/issues/9185. For hevc it's a software issue that has been fixed but not released yet, see: https://gitlab.freedesktop.org/mesa/mesa/-/issues/10985.
+If you get black bars then the workaround is to record with h264 video codec instead (using the -k h264 option).
+## The video is glitched, looks like checkerboard pattern
+This is an issue on some intel integrated gpus on wayland caused by power saving option. Right now the only way to fix this is to record on X11 instead.
+## The video doesn't display or has a green/yellow overlay
+This can happen if your video player is missing the H264/HEVC video codecs. Either install the codecs or use mpv.
+## I get stutter in the video
+Try recording to an SSD and make sure it's not using NTFS file system. Also record in variable framerate format.
+## I get a black screen when recording
+This can happen if you use software such as prime-run to run GPU Screen Recorder. Such software should not be used to run GPU Screen Recorder.
+GPU Screen Recorder needs to run on the same GPU that you use to display your monitors graphics to work.
+
+# Donations
+If you want to donate you can donate via bitcoin or monero.
+* Bitcoin: bc1qqvuqnwrdyppf707ge27fqz2n9y9gu7lf5ypyuf
+* Monero: 4An9kp2qW1C9Gah7ewv4JzcNFQ5TAX7ineGCqXWK6vQnhsGGcRpNgcn8r9EC3tMcgY7vqCKs3nSRXhejMHBaGvFdN2egYet
# TODO
-* Support AMD and Intel, using VAAPI. cuda and vaapi should be loaded at runtime using dlopen instead of linking to those
-libraries at compile-time.
-* Clean up the code!
-* Fix segfault in debug mode (happens because audio codec becomes NULL?)
* Dynamically change bitrate/resolution to match desired fps. This would be helpful when streaming for example, where the encode output speed also depends on upload speed to the streaming service.
-* Show cursor when recording. Currently the cursor is not visible when recording a window and it's disabled when recording screen-direct to allow direct nvfbc capture for fullscreen windows, which allows for better performance and variable refresh rate monitors to work.
-* Implement opengl injection to capture texture. This fixes composition issues and (VRR) without having to use NvFBC direct capture.
-* Always use direct capture with NvFBC once the capture issue in mpv fullscreen has been resolved.
+* Implement opengl injection to capture texture. This fixes VRR without having to use NvFBC direct capture.
+* Always use direct capture with NvFBC once the capture issue in mpv fullscreen has been resolved (maybe detect if direct capture fails in nvfbc and switch to non-direct recording. NvFBC says if direct capture fails).
diff --git a/TODO b/TODO
index 6b432e9..5211e9f 100644
--- a/TODO
+++ b/TODO
@@ -1,8 +1,123 @@
Check for reparent.
-Only add window to list if its the window is a topmost window.
-Load cuda at runtime with dlopen.
-Track window damages and only update then. That is better for output file size.
-Getting the texture of a window when using a compositor is an nvidia specific limitation. When gpu-screen-recorder supports other gpus then this can be ignored.
-Remove dependency on glfw (and glew?).
Quickly changing workspace and back while recording under i3 breaks the screen recorder. i3 probably unmaps windows in other workspaces.
-Remove hw_get_frame as it creates a new cuda device ptr which we dont use!
+See https://trac.ffmpeg.org/wiki/EncodingForStreamingSites for optimizing streaming.
+Look at VK_EXT_external_memory_dma_buf.
+Allow setting a different output resolution than the input resolution.
+Use mov+faststart.
+Allow recording all monitors/selected monitor without nvfbc by recording the compositor proxy window and only recording the part that matches the monitor(s).
+Allow recording a region by recording the compositor proxy window / nvfbc window and copying part of it.
+Use nvenc directly, which allows removing the use of cuda.
+Handle xrandr monitor change in nvfbc.
+Implement follow focused in drm.
+Support amf and qsv.
+Disable flipping on nvidia? this might fix some stuttering issues on some setups. See NvCtrlGetAttribute/NvCtrlSetAttributeAndGetStatus NV_CTRL_SYNC_TO_VBLANK https://github.com/NVIDIA/nvidia-settings/blob/d5f022976368cbceb2f20b838ddb0bf992f0cfb9/src/gtk%2B-2.x/ctkopengl.c.
+Replays seem to have some issues with audio/video. Why?
+Cleanup unused gl/egl functions, macro, etc.
+Add option to disable overlapping of replays (the old behavior kinda. Remove the whole replay buffer data after saving when doing this).
+Set audio track name to audio device name (if not merge of multiple audio devices).
+Add support for webcam, but only really for amd/intel because amd/intel can get drm fd access to webcam, nvidia cant. This allows us to create an opengl texture directly from the webcam fd for optimal performance.
+Reverse engineer nvapi so we can disable "force p2 state" on linux too (nvapi profile api with the settings id 0x50166c5e).
+Support yuv444p on amd/intel.
+fix yuv444 for hevc.
+Do not allow streaming if yuv444.
+Re-enable yuv444.
+Support 10 bit output because of better gradients. May even be smaller file size. Better supported on hevc (not supported at all on h264 on my gpu).
+Add nvidia/(amd/intel) specific install script for ubuntu. User should run install_ubuntu.sh but it should run different install dep script depending on if /proc/driver/nvidia/version exists or not. But what about switchable graphics setup?
+Test different combinations of switchable graphics. Intel hybrid mode (running intel but possible to run specific applications with prime-run), running pure intel. Detect switchable graphics.
+
+https://web.archive.org/web/20210306020203/https://forums.developer.nvidia.com/t/performance-power-management-problem-on-shared-vgpu/161986
+https://djdallmann.github.io/GamingPCSetup/CONTENT/RESEARCH/FINDINGS/registrykeys_displayadapter_class_4d36e968-e325-11ce-bfc1-08002be10318.txt
+
+The video output will be black if if the system is suspended on nvidia and NVreg_PreserveVideoMemoryAllocations is not set to 1. This happens because I think that the driver invalidates textures/cuda buffers? To fix this we could try and recreate gsr capture when gsr_capture_capture fails (with timeout to retry again).
+
+NVreg_RegistryDwords.
+Restore nvfbc screen recording on monitor reconfiguration.
+Window capture doesn't work properly in _control_ game after going from pause menu to in-game (and back to pause menu). There might be some x11 event we need to catch. Same for vr-video-player.
+
+Monitor capture on steam deck is slightly below the game fps, but only when capturing on the steam deck screen. If capturing on another monitor, there is no issue.
+ Is this related to the dma buf rotation issue? different modifier being slow? does this always happen?
+
+Fallback to vaapi copy in kms if opengl version fails. This can happen on steam deck for some reason (driver bug?). Also vaapi copy uses less gpu since it uses video codec unit to copy.
+Test if vaapi copy version uses less memory than opengl version.
+
+Intel is a bit weird with monitor capture and multiple monitors. If one of the monitors is rotated then all the kms will be rotated as well.
+ Is that only the case when the primary monitor is rotated? Also the primary monitor becomes position 0, 0 so crtc (x11 randr) position doesn't match the drm pos. Maybe get monitor position and size from drm instead.
+ How about if multiple monitors are rotated?
+
+Support vp8/vp9. This is especially important on amd which on some distros (such as Manjaro) where hardware accelerated h264/hevc is disabled in the mesa package.
+
+Support screen (all monitors) capture on amd/intel and nvidia wayland when no combined plane is found. Right now screen just takes the first output.
+Use separate plane (which has offset and pitch) from combined plane instead of the combined plane.
+
+Both twitch and youtube support variable bitrate but twitch recommends constant bitrate to reduce stream buffering/dropped frames when going from low motion to high motion: https://help.twitch.tv/s/article/broadcasting-guidelines?language=en_US. Info for youtube: https://support.google.com/youtube/answer/2853702?hl=en#zippy=%2Cvariable-bitrate-with-custom-stream-keys-in-live-control-room%2Ck-p-fps%2Cp-fps.
+
+Limit fps recording with x damage. This is good when running replay mode 24/7 and being afk or when not much is happening on the screen.
+
+On nvidia some games apparently causes the game to appear to stutter (without dropping fps) when recording a monitor but not using
+ when using direct screen capture. Observed in Deus Ex and Apex Legends.
+
+Support "screen" (all monitors) capture on wayland. This should be done by getting all drm fds and multiple EGL_DMA_BUF_PLANEX_FD_EXT to create one egl image with all fds combined.
+
+Support pipewire screen capture?
+CPU usage is pretty high on AMD/Intel/(Nvidia(wayland)), why? opening and closing fds, creating egl, cuda association, is slow when done every frame. Test if desktop portal screencast has better performance.
+
+Capture is broken on amd on wlroots. It's disabled at the moment and instead uses kms capture. Find out why we get a black screen in wlroots.
+
+Support vulkan video encoding. That might workaround forced p2 state nvidia driver "bug". Ffmpeg supports vulkan video encoding if it's encoding with --enable-vulkan
+
+It may be possible to improve color conversion rgb->yuv shader for color edges by biasing colors to an edge, instead of letting color overlaying with bilinear filtering handle it.
+
+When webcam is supported mention that nvidia_drm.modeset=1 must be set on nvidia x11 (it's required on wayland so it's not needed there. Or does eglstream work without it??). Check if this really is the case.
+ Support green screen removal, cropping, shader effects in general (circle mask, rounded corners, etc).
+
+Preset is set to p5 for now but it should ideally be p6 or p7.
+ This change is needed because for certain sizes of a window (or monitor?) such as 971x780 causes encoding to freeze
+ when using h264 codec. This is a new(?) nvidia driver bug.
+ Maybe dont choose p6 or p7 again? it causes micro stutter for some users (?).
+
+For low latency, see https://developer.download.nvidia.com/compute/nvenc/v4.0/NVENC_VideoEncoder_API_ProgGuide.pdf (section 7.1).
+Remove follow focused option.
+
+Exit if X11/Wayland killed (if drm plane dead or something?)
+
+Use SRC_W and SRC_H for screen plane instead of crtc_w and crtc_h.
+
+Make it possible to select which /dev/dri/card* to use, but that requires opengl to also use the same card. Not sure if that is possible for amd, intel and nvidia without using vulkan instead.
+
+Support I915_FORMAT_MOD_Y_TILED_CCS (and other power saving modifiers, see https://trac.ffmpeg.org/ticket/8542). The only fix may be to use desktop portal for recording. This issue doesn't appear on x11 since these modifiers are not used by xorg server.
+
+Test if p2 state can be worked around by using pure nvenc api and overwriting cuInit/cuCtxCreate* to not do anything. Cuda might be loaded when using nvenc but it might not be used, with certain record options? (such as h264 p5).
+ nvenc uses cuda when using b frames and rgb->yuv conversion, so convert the image ourselves instead.-
+
+Mesa doesn't support global headers (AV_CODEC_FLAG_GLOBAL_HEADER) with h264... which also breaks mkv since mkv requires global header. Right now gpu screen recorder will forcefully set video codec to hevc when h264 is requested for mkv files.
+
+Drop frames if live streaming cant keep up with target fps, or dynamically change resolution/quality.
+
+Support low power option (does it even work with vaapi in ffmpeg??). Would be very useful for steam deck.
+
+Instead of sending a big list of drm data back to kms client, send the monitor we want to record to kms server and the server should respond with only the matching monitor, and cursor.
+
+Tonemap hdr to sdr when hdr is enabled and when hevc_hdr/av1_hdr is not used.
+
+Add 10 bit record option, h264_10bit, hevc_10bit and av1_10bit.
+
+Rotate cursor texture properly (around top left origin).
+
+Setup hardware video context so we can query constraints and capabilities for better default and better error messages.
+
+Use CAP_SYS_NICE in flatpak too on the main gpu screen recorder binary. It makes recording smoother, especially with constant framerate.
+
+Show error when using compressed kms plane which isn't supported. Also do that in the gui.
+
+Modify ffmpeg to accept opengl texture for nvenc encoding. Removes extra buffers and copies.
+
+When vulkan encode is added, mention minimum nvidia driver required. (550.54.14?).
+
+Support drm plane rotation. Neither X11 nor any Wayland compositor currently rotates drm planes so this might not be needed.
+
+Investigate if there is a way to do gpu->gpu copy directly without touching system ram to enable video encoding on a different gpu. On nvidia this is possible with cudaMemcpyPeer, but how about from an intel/amd gpu to an nvidia gpu or the other way around or any combination of iGPU and dedicated GPU?
+ Maybe something with clEnqueueMigrateMemObjects? on AMD something with DirectGMA maybe?
+
+Fix opus/flac ( variable framerate audio :( ). Going back to constant framerate audio should fix the issue with skipped frames when recording for some people (issue only reproducable with pulseaudio, and only for some users?).
+
+Go back to using pure vaapi without opengl for video encoding? rotation (transpose) can be done if its done after (rgb to yuv) color conversion.
diff --git a/build.sh b/build.sh
index 9bdaf16..b353406 100755
--- a/build.sh
+++ b/build.sh
@@ -1,8 +1,53 @@
#!/bin/sh -e
-dependencies="glew libavcodec libavformat libavutil x11 xcomposite glfw3 libpulse-simple libswresample"
-includes="$(pkg-config --cflags $dependencies) -I/opt/cuda/targets/x86_64-linux/include"
-libs="$(pkg-config --libs $dependencies) /usr/lib64/libcuda.so -ldl -pthread -lm"
-g++ -c src/sound.cpp -O2 $includes -DPULSEAUDIO=1
-g++ -c src/main.cpp -O2 $includes -DPULSEAUDIO=1
-g++ -o gpu-screen-recorder -O2 sound.o main.o -s $libs
+script_dir=$(dirname "$0")
+cd "$script_dir"
+
+CC=${CC:-gcc}
+CXX=${CXX:-g++}
+
+opts="-O2 -g0 -DNDEBUG -Wall -Wextra -Wshadow"
+[ -n "$DEBUG" ] && opts="-O0 -g3 -Wall -Wextra -Wshadow";
+
+build_gsr_kms_server() {
+ # TODO: -fcf-protection=full, not supported on arm
+ extra_opts="-fstack-protector-all"
+ dependencies="libdrm"
+ includes="$(pkg-config --cflags $dependencies)"
+ libs="$(pkg-config --libs $dependencies) -ldl"
+ $CC -c kms/server/kms_server.c $opts $extra_opts $includes
+ $CC -o gsr-kms-server kms_server.o $libs $opts $extra_opts
+}
+
+build_gsr() {
+ dependencies="libavcodec libavformat libavutil x11 xcomposite xrandr xfixes libpulse libswresample libavfilter libva libcap libdrm wayland-egl wayland-client"
+ includes="$(pkg-config --cflags $dependencies)"
+ libs="$(pkg-config --libs $dependencies) -ldl -pthread -lm"
+ $CC -c src/capture/capture.c $opts $includes
+ $CC -c src/capture/nvfbc.c $opts $includes
+ $CC -c src/capture/xcomposite.c $opts $includes
+ $CC -c src/capture/xcomposite_cuda.c $opts $includes
+ $CC -c src/capture/xcomposite_vaapi.c $opts $includes
+ $CC -c src/capture/kms_vaapi.c $opts $includes
+ $CC -c src/capture/kms_cuda.c $opts $includes
+ $CC -c src/capture/kms.c $opts $includes
+ $CC -c kms/client/kms_client.c $opts $includes
+ $CC -c src/egl.c $opts $includes
+ $CC -c src/cuda.c $opts $includes
+ $CC -c src/xnvctrl.c $opts $includes
+ $CC -c src/overclock.c $opts $includes
+ $CC -c src/window_texture.c $opts $includes
+ $CC -c src/shader.c $opts $includes
+ $CC -c src/color_conversion.c $opts $includes
+ $CC -c src/utils.c $opts $includes
+ $CC -c src/library_loader.c $opts $includes
+ $CC -c src/cursor.c $opts $includes
+ $CXX -c src/sound.cpp $opts $includes
+ $CXX -c src/main.cpp $opts $includes
+ $CXX -o gpu-screen-recorder capture.o nvfbc.o kms_client.o egl.o cuda.o xnvctrl.o overclock.o window_texture.o shader.o \
+ color_conversion.o utils.o library_loader.o cursor.o xcomposite.o xcomposite_cuda.o xcomposite_vaapi.o kms_vaapi.o kms_cuda.o kms.o sound.o main.o $libs $opts
+}
+
+build_gsr_kms_server
+build_gsr
+echo "Successfully built gpu-screen-recorder"
diff --git a/debug-install.sh b/debug-install.sh
new file mode 100755
index 0000000..096eefa
--- /dev/null
+++ b/debug-install.sh
@@ -0,0 +1,20 @@
+#!/bin/sh -e
+
+script_dir=$(dirname "$0")
+cd "$script_dir"
+
+[ $(id -u) -ne 0 ] && echo "You need root privileges to run the install script" && exit 1
+
+DEBUG=1 ./build.sh
+
+install -Dm755 "gsr-kms-server" "/usr/bin/gsr-kms-server"
+install -Dm755 "gpu-screen-recorder" "/usr/bin/gpu-screen-recorder"
+if [ -d "/usr/lib/systemd/user" ]; then
+ install -Dm644 "extra/gpu-screen-recorder.service" "/usr/lib/systemd/user/gpu-screen-recorder.service"
+fi
+# Not necessary, but removes the password prompt when trying to record a monitor on amd/intel or nvidia wayland
+setcap cap_sys_admin+ep /usr/bin/gsr-kms-server
+# Not necessary, but allows use of EGL_CONTEXT_PRIORITY_LEVEL_IMG which allows gpu screen recorder to run without being limited to game fps under heavy load on AMD/Intel
+setcap cap_sys_nice+ep /usr/bin/gpu-screen-recorder
+
+echo "Successfully installed gpu-screen-recorder (debug)"
diff --git a/include/NvFBC.h b/external/NvFBC.h
index 8990eea..8990eea 100644
--- a/include/NvFBC.h
+++ b/external/NvFBC.h
diff --git a/extra/gpu-screen-recorder.env b/extra/gpu-screen-recorder.env
new file mode 100644
index 0000000..c4728cc
--- /dev/null
+++ b/extra/gpu-screen-recorder.env
@@ -0,0 +1,9 @@
+WINDOW=screen
+CONTAINER=mp4
+QUALITY=very_high
+CODEC=hevc
+AUDIO_CODEC=opus
+AUDIO_DEVICE=alsa_output.pci-0000_0a_00.4.iec958-stereo.monitor
+FRAMERATE=60
+REPLAYDURATION=60
+OUTPUTDIR=/run/media/dec05eba/SSD1TB/Videos/aaaa \ No newline at end of file
diff --git a/extra/gpu-screen-recorder.service b/extra/gpu-screen-recorder.service
new file mode 100644
index 0000000..0ba621b
--- /dev/null
+++ b/extra/gpu-screen-recorder.service
@@ -0,0 +1,23 @@
+[Unit]
+Description=GPU Screen Recorder Service
+
+[Service]
+EnvironmentFile=-%h/.config/gpu-screen-recorder.env
+Environment=WINDOW=screen
+Environment=CONTAINER=mp4
+Environment=QUALITY=very_high
+Environment=CODEC=auto
+Environment=AUDIO_CODEC=opus
+Environment=AUDIO_DEVICE=
+Environment=FRAMERATE=60
+Environment=REPLAYDURATION=30
+Environment=OUTPUTDIR=%h/Videos
+Environment=MAKEFOLDERS=no
+Environment=COLOR_RANGE=limited
+ExecStart=/bin/sh -c 'AUDIO="${AUDIO_DEVICE:-$(pactl get-default-sink).monitor}"; gpu-screen-recorder -v no -w $WINDOW -c $CONTAINER -q $QUALITY -k $CODEC -ac $AUDIO_CODEC -a "$AUDIO" -f $FRAMERATE -r $REPLAYDURATION -o "$OUTPUTDIR" -mf $MAKEFOLDERS $ADDITIONAL_ARGS -cr $COLOR_RANGE'
+KillSignal=SIGINT
+Restart=on-failure
+RestartSec=5s
+
+[Install]
+WantedBy=default.target
diff --git a/extra/gsr-nvidia.conf b/extra/gsr-nvidia.conf
new file mode 100644
index 0000000..10cbf7d
--- /dev/null
+++ b/extra/gsr-nvidia.conf
@@ -0,0 +1 @@
+options nvidia NVreg_PreserveVideoMemoryAllocations=1
diff --git a/extra/install_preserve_video_memory.sh b/extra/install_preserve_video_memory.sh
new file mode 100755
index 0000000..c5cf658
--- /dev/null
+++ b/extra/install_preserve_video_memory.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+script_dir=$(dirname "$0")
+cd "$script_dir"
+
+[ $(id -u) -ne 0 ] && echo "You need root privileges to run the install script" && exit 1
+
+install -Dm644 gsr-nvidia.conf /etc/modprobe.d/gsr-nvidia.conf
diff --git a/include/NvFBCLibrary.hpp b/include/NvFBCLibrary.hpp
deleted file mode 100644
index 6d115a4..0000000
--- a/include/NvFBCLibrary.hpp
+++ /dev/null
@@ -1,246 +0,0 @@
-#pragma once
-
-#include "NvFBC.h"
-#include <cuda.h>
-
-#include <dlfcn.h>
-#include <string.h>
-#include <stdio.h>
-
-class NvFBCLibrary {
-public:
- ~NvFBCLibrary() {
- if(fbc_handle_created) {
- NVFBC_DESTROY_CAPTURE_SESSION_PARAMS destroy_capture_params;
- memset(&destroy_capture_params, 0, sizeof(destroy_capture_params));
- destroy_capture_params.dwVersion = NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER;
- nv_fbc_function_list.nvFBCDestroyCaptureSession(nv_fbc_handle, &destroy_capture_params);
-
- NVFBC_DESTROY_HANDLE_PARAMS destroy_params;
- memset(&destroy_params, 0, sizeof(destroy_params));
- destroy_params.dwVersion = NVFBC_DESTROY_HANDLE_PARAMS_VER;
- nv_fbc_function_list.nvFBCDestroyHandle(nv_fbc_handle, &destroy_params);
- }
-
- if(library)
- dlclose(library);
- }
-
- bool load() {
- if(library)
- return true;
-
- dlerror(); // clear
- void *lib = dlopen("libnvidia-fbc.so.1", RTLD_NOW);
- if(!lib) {
- fprintf(stderr, "Error: failed to load libnvidia-fbc.so.1, error: %s\n", dlerror());
- return false;
- }
-
- nv_fbc_create_instance = (PNVFBCCREATEINSTANCE)dlsym(lib, "NvFBCCreateInstance");
- if(!nv_fbc_create_instance) {
- fprintf(stderr, "Error: unable to resolve symbol 'NvFBCCreateInstance'\n");
- dlclose(lib);
- return false;
- }
-
- memset(&nv_fbc_function_list, 0, sizeof(nv_fbc_function_list));
- nv_fbc_function_list.dwVersion = NVFBC_VERSION;
- NVFBCSTATUS status = nv_fbc_create_instance(&nv_fbc_function_list);
- if(status != NVFBC_SUCCESS) {
- fprintf(stderr, "Error: failed to create NvFBC instance (status: %d)\n", status);
- dlclose(lib);
- return false;
- }
-
- library = lib;
- return true;
- }
-
- // If |display_to_capture| is "screen", then the entire x11 screen is captured (all displays)
- bool create(const char *display_to_capture, uint32_t fps, /*out*/ uint32_t *display_width, /*out*/ uint32_t *display_height, uint32_t x = 0, uint32_t y = 0, uint32_t width = 0, uint32_t height = 0, bool direct_capture = false) {
- if(!library || !display_to_capture || !display_width || !display_height || fbc_handle_created)
- return false;
-
- const bool capture_region = (x > 0 || y > 0 || width > 0 || height > 0);
-
- NVFBCSTATUS status;
- NVFBC_TRACKING_TYPE tracking_type;
- bool capture_session_created = false;
- uint32_t output_id = 0;
- fbc_handle_created = false;
-
- NVFBC_CREATE_HANDLE_PARAMS create_params;
- memset(&create_params, 0, sizeof(create_params));
- create_params.dwVersion = NVFBC_CREATE_HANDLE_PARAMS_VER;
-
- status = nv_fbc_function_list.nvFBCCreateHandle(&nv_fbc_handle, &create_params);
- if(status != NVFBC_SUCCESS) {
- fprintf(stderr, "Error: %s\n", nv_fbc_function_list.nvFBCGetLastErrorStr(nv_fbc_handle));
- return false;
- }
- fbc_handle_created = true;
-
- NVFBC_GET_STATUS_PARAMS status_params;
- memset(&status_params, 0, sizeof(status_params));
- status_params.dwVersion = NVFBC_GET_STATUS_PARAMS_VER;
-
- status = nv_fbc_function_list.nvFBCGetStatus(nv_fbc_handle, &status_params);
- if(status != NVFBC_SUCCESS) {
- fprintf(stderr, "Error: %s\n", nv_fbc_function_list.nvFBCGetLastErrorStr(nv_fbc_handle));
- goto error_cleanup;
- }
-
- if(status_params.bCanCreateNow == NVFBC_FALSE) {
- fprintf(stderr, "Error: it's not possible to create a capture session on this system\n");
- goto error_cleanup;
- }
-
- tracking_type = strcmp(display_to_capture, "screen") == 0 ? NVFBC_TRACKING_SCREEN : NVFBC_TRACKING_OUTPUT;
- if(tracking_type == NVFBC_TRACKING_OUTPUT) {
- if(!status_params.bXRandRAvailable) {
- fprintf(stderr, "Error: the xrandr extension is not available\n");
- goto error_cleanup;
- }
-
- if(status_params.bInModeset) {
- fprintf(stderr, "Error: the x server is in modeset, unable to record\n");
- goto error_cleanup;
- }
-
- output_id = get_output_id_from_display_name(status_params.outputs, status_params.dwOutputNum, display_to_capture, display_width, display_height);
- if(output_id == 0) {
- fprintf(stderr, "Error: display '%s' not found\n", display_to_capture);
- goto error_cleanup;
- }
- } else {
- *display_width = status_params.screenSize.w;
- *display_height = status_params.screenSize.h;
- }
-
- NVFBC_CREATE_CAPTURE_SESSION_PARAMS create_capture_params;
- memset(&create_capture_params, 0, sizeof(create_capture_params));
- create_capture_params.dwVersion = NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER;
- create_capture_params.eCaptureType = NVFBC_CAPTURE_SHARED_CUDA;
- create_capture_params.bWithCursor = direct_capture ? NVFBC_FALSE : NVFBC_TRUE;
- if(capture_region) {
- create_capture_params.captureBox = { x, y, width, height };
- *display_width = width;
- *display_height = height;
- }
- create_capture_params.eTrackingType = tracking_type;
- create_capture_params.dwSamplingRateMs = 1000 / fps;
- create_capture_params.bAllowDirectCapture = direct_capture ? NVFBC_TRUE : NVFBC_FALSE;
- create_capture_params.bPushModel = direct_capture ? NVFBC_TRUE : NVFBC_FALSE;
- if(tracking_type == NVFBC_TRACKING_OUTPUT)
- create_capture_params.dwOutputId = output_id;
-
- status = nv_fbc_function_list.nvFBCCreateCaptureSession(nv_fbc_handle, &create_capture_params);
- if(status != NVFBC_SUCCESS) {
- fprintf(stderr, "Error: %s\n", nv_fbc_function_list.nvFBCGetLastErrorStr(nv_fbc_handle));
- goto error_cleanup;
- }
- capture_session_created = true;
-
- NVFBC_TOCUDA_SETUP_PARAMS setup_params;
- memset(&setup_params, 0, sizeof(setup_params));
- setup_params.dwVersion = NVFBC_TOCUDA_SETUP_PARAMS_VER;
- setup_params.eBufferFormat = NVFBC_BUFFER_FORMAT_BGRA;
-
- status = nv_fbc_function_list.nvFBCToCudaSetUp(nv_fbc_handle, &setup_params);
- if(status != NVFBC_SUCCESS) {
- fprintf(stderr, "Error: %s\n", nv_fbc_function_list.nvFBCGetLastErrorStr(nv_fbc_handle));
- goto error_cleanup;
- }
-
- return true;
-
- error_cleanup:
- if(fbc_handle_created) {
- if(capture_session_created) {
- NVFBC_DESTROY_CAPTURE_SESSION_PARAMS destroy_capture_params;
- memset(&destroy_capture_params, 0, sizeof(destroy_capture_params));
- destroy_capture_params.dwVersion = NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER;
- nv_fbc_function_list.nvFBCDestroyCaptureSession(nv_fbc_handle, &destroy_capture_params);
- }
-
- NVFBC_DESTROY_HANDLE_PARAMS destroy_params;
- memset(&destroy_params, 0, sizeof(destroy_params));
- destroy_params.dwVersion = NVFBC_DESTROY_HANDLE_PARAMS_VER;
- nv_fbc_function_list.nvFBCDestroyHandle(nv_fbc_handle, &destroy_params);
- fbc_handle_created = false;
- }
- output_id = 0;
- return false;
- }
-
- bool capture(/*out*/ CUdeviceptr *cu_device_ptr, uint32_t *byte_size) {
- if(!library || !fbc_handle_created || !cu_device_ptr || !byte_size)
- return false;
-
- NVFBCSTATUS status;
- NVFBC_FRAME_GRAB_INFO frame_info;
- memset(&frame_info, 0, sizeof(frame_info));
-
- NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab_params;
- memset(&grab_params, 0, sizeof(grab_params));
- grab_params.dwVersion = NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER;
- grab_params.dwFlags = NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT;
- grab_params.pFrameGrabInfo = &frame_info;
- grab_params.pCUDADeviceBuffer = cu_device_ptr;
-
- status = nv_fbc_function_list.nvFBCToCudaGrabFrame(nv_fbc_handle, &grab_params);
- if(status != NVFBC_SUCCESS) {
- fprintf(stderr, "Error: %s\n", nv_fbc_function_list.nvFBCGetLastErrorStr(nv_fbc_handle));
- return false;
- }
-
- *byte_size = frame_info.dwByteSize;
- // TODO: Check bIsNewFrame
- // TODO: Check dwWidth and dwHeight and update size in video output in ffmpeg. This can happen when xrandr is used to change monitor resolution
-
- return true;
- }
-private:
- static char to_upper(char c) {
- if(c >= 'a' && c <= 'z')
- return c - 32;
- else
- return c;
- }
-
- static bool strcase_equals(const char *str1, const char *str2) {
- for(;;) {
- char c1 = to_upper(*str1);
- char c2 = to_upper(*str2);
- if(c1 != c2)
- return false;
- if(c1 == '\0' || c2 == '\0')
- return true;
- ++str1;
- ++str2;
- }
- }
-
- // Returns 0 on failure
- uint32_t get_output_id_from_display_name(NVFBC_RANDR_OUTPUT_INFO *outputs, uint32_t num_outputs, const char *display_name, uint32_t *display_width, uint32_t *display_height) {
- if(!outputs)
- return 0;
-
- for(uint32_t i = 0; i < num_outputs; ++i) {
- if(strcase_equals(outputs[i].name, display_name)) {
- *display_width = outputs[i].trackedBox.w;
- *display_height = outputs[i].trackedBox.h;
- return outputs[i].dwId;
- }
- }
-
- return 0;
- }
-private:
- void *library = nullptr;
- PNVFBCCREATEINSTANCE nv_fbc_create_instance = nullptr;
- NVFBC_API_FUNCTION_LIST nv_fbc_function_list;
- NVFBC_SESSION_HANDLE nv_fbc_handle;
- bool fbc_handle_created = false;
-}; \ No newline at end of file
diff --git a/include/capture/capture.h b/include/capture/capture.h
new file mode 100644
index 0000000..2eb8e42
--- /dev/null
+++ b/include/capture/capture.h
@@ -0,0 +1,68 @@
+#ifndef GSR_CAPTURE_CAPTURE_H
+#define GSR_CAPTURE_CAPTURE_H
+
+#include "../color_conversion.h"
+#include <stdbool.h>
+
+typedef struct AVCodecContext AVCodecContext;
+typedef struct AVFrame AVFrame;
+typedef void* VADisplay;
+typedef struct _VADRMPRIMESurfaceDescriptor VADRMPRIMESurfaceDescriptor;
+typedef struct gsr_cuda gsr_cuda;
+typedef struct AVFrame AVFrame;
+typedef struct CUgraphicsResource_st *CUgraphicsResource;
+typedef struct CUarray_st *CUarray;
+typedef struct CUctx_st *CUcontext;
+typedef struct CUstream_st *CUstream;
+
+typedef struct gsr_capture gsr_capture;
+
+struct gsr_capture {
+ /* These methods should not be called manually. Call gsr_capture_* instead */
+ int (*start)(gsr_capture *cap, AVCodecContext *video_codec_context, AVFrame *frame);
+ void (*tick)(gsr_capture *cap, AVCodecContext *video_codec_context); /* can be NULL */
+ bool (*should_stop)(gsr_capture *cap, bool *err); /* can be NULL */
+ int (*capture)(gsr_capture *cap, AVFrame *frame);
+ void (*capture_end)(gsr_capture *cap, AVFrame *frame); /* can be NULL */
+ void (*destroy)(gsr_capture *cap, AVCodecContext *video_codec_context);
+
+ void *priv; /* can be NULL */
+ bool started;
+};
+
+typedef struct gsr_capture_base gsr_capture_base;
+
+struct gsr_capture_base {
+ gsr_egl *egl;
+
+ unsigned int input_texture;
+ unsigned int target_textures[2];
+ unsigned int cursor_texture;
+
+ gsr_color_conversion color_conversion;
+
+ AVCodecContext *video_codec_context;
+};
+
+typedef struct {
+ gsr_cuda *cuda;
+ CUgraphicsResource *cuda_graphics_resources;
+ CUarray *mapped_arrays;
+} gsr_cuda_context;
+
+int gsr_capture_start(gsr_capture *cap, AVCodecContext *video_codec_context, AVFrame *frame);
+void gsr_capture_tick(gsr_capture *cap, AVCodecContext *video_codec_context);
+bool gsr_capture_should_stop(gsr_capture *cap, bool *err);
+int gsr_capture_capture(gsr_capture *cap, AVFrame *frame);
+void gsr_capture_end(gsr_capture *cap, AVFrame *frame);
+/* Calls |gsr_capture_stop| as well */
+void gsr_capture_destroy(gsr_capture *cap, AVCodecContext *video_codec_context);
+
+bool gsr_capture_base_setup_vaapi_textures(gsr_capture_base *self, AVFrame *frame, VADisplay va_dpy, VADRMPRIMESurfaceDescriptor *prime, gsr_color_range color_range);
+bool gsr_capture_base_setup_cuda_textures(gsr_capture_base *self, AVFrame *frame, gsr_cuda_context *cuda_context, gsr_color_range color_range, gsr_source_color source_color, bool hdr);
+void gsr_capture_base_stop(gsr_capture_base *self);
+
+bool drm_create_codec_context(const char *card_path, AVCodecContext *video_codec_context, int width, int height, bool hdr, VADisplay *va_dpy);
+bool cuda_create_codec_context(CUcontext cu_ctx, AVCodecContext *video_codec_context, int width, int height, bool hdr, CUstream *cuda_stream);
+
+#endif /* GSR_CAPTURE_CAPTURE_H */
diff --git a/include/capture/kms.h b/include/capture/kms.h
new file mode 100644
index 0000000..674813a
--- /dev/null
+++ b/include/capture/kms.h
@@ -0,0 +1,50 @@
+#ifndef GSR_CAPTURE_KMS_H
+#define GSR_CAPTURE_KMS_H
+
+#include "capture.h"
+#include "../../kms/client/kms_client.h"
+#include "../color_conversion.h"
+#include "../vec2.h"
+#include "../defs.h"
+#include <stdbool.h>
+
+typedef struct AVCodecContext AVCodecContext;
+typedef struct AVMasteringDisplayMetadata AVMasteringDisplayMetadata;
+typedef struct AVContentLightMetadata AVContentLightMetadata;
+typedef struct gsr_capture_kms gsr_capture_kms;
+typedef struct gsr_egl gsr_egl;
+typedef struct AVFrame AVFrame;
+
+#define MAX_CONNECTOR_IDS 32
+
+typedef struct {
+ uint32_t connector_ids[MAX_CONNECTOR_IDS];
+ int num_connector_ids;
+} MonitorId;
+
+struct gsr_capture_kms {
+ gsr_capture_base base;
+
+ bool should_stop;
+ bool stop_is_error;
+
+ gsr_kms_client kms_client;
+ gsr_kms_response kms_response;
+
+ vec2i capture_pos;
+ vec2i capture_size;
+ MonitorId monitor_id;
+
+ AVMasteringDisplayMetadata *mastering_display_metadata;
+ AVContentLightMetadata *light_metadata;
+
+ gsr_monitor_rotation monitor_rotation;
+};
+
+/* Returns 0 on success */
+int gsr_capture_kms_start(gsr_capture_kms *self, const char *display_to_capture, gsr_egl *egl, AVCodecContext *video_codec_context, AVFrame *frame);
+void gsr_capture_kms_stop(gsr_capture_kms *self);
+bool gsr_capture_kms_capture(gsr_capture_kms *self, AVFrame *frame, bool hdr, bool screen_plane_use_modifiers, bool cursor_texture_is_external, bool record_cursor);
+void gsr_capture_kms_cleanup_kms_fds(gsr_capture_kms *self);
+
+#endif /* GSR_CAPTURE_KMS_H */
diff --git a/include/capture/kms_cuda.h b/include/capture/kms_cuda.h
new file mode 100644
index 0000000..fd0d396
--- /dev/null
+++ b/include/capture/kms_cuda.h
@@ -0,0 +1,20 @@
+#ifndef GSR_CAPTURE_KMS_CUDA_H
+#define GSR_CAPTURE_KMS_CUDA_H
+
+#include "../vec2.h"
+#include "../utils.h"
+#include "../color_conversion.h"
+#include "capture.h"
+
+typedef struct {
+ gsr_egl *egl;
+ const char *display_to_capture; /* if this is "screen", then the first monitor is captured. A copy is made of this */
+ gsr_gpu_info gpu_inf;
+ bool hdr;
+ gsr_color_range color_range;
+ bool record_cursor;
+} gsr_capture_kms_cuda_params;
+
+gsr_capture* gsr_capture_kms_cuda_create(const gsr_capture_kms_cuda_params *params);
+
+#endif /* GSR_CAPTURE_KMS_CUDA_H */
diff --git a/include/capture/kms_vaapi.h b/include/capture/kms_vaapi.h
new file mode 100644
index 0000000..196b597
--- /dev/null
+++ b/include/capture/kms_vaapi.h
@@ -0,0 +1,20 @@
+#ifndef GSR_CAPTURE_KMS_VAAPI_H
+#define GSR_CAPTURE_KMS_VAAPI_H
+
+#include "../vec2.h"
+#include "../utils.h"
+#include "../color_conversion.h"
+#include "capture.h"
+
+typedef struct {
+ gsr_egl *egl;
+ const char *display_to_capture; /* if this is "screen", then the first monitor is captured. A copy is made of this */
+ gsr_gpu_info gpu_inf;
+ bool hdr;
+ gsr_color_range color_range;
+ bool record_cursor;
+} gsr_capture_kms_vaapi_params;
+
+gsr_capture* gsr_capture_kms_vaapi_create(const gsr_capture_kms_vaapi_params *params);
+
+#endif /* GSR_CAPTURE_KMS_VAAPI_H */
diff --git a/include/capture/nvfbc.h b/include/capture/nvfbc.h
new file mode 100644
index 0000000..36bc2b6
--- /dev/null
+++ b/include/capture/nvfbc.h
@@ -0,0 +1,22 @@
+#ifndef GSR_CAPTURE_NVFBC_H
+#define GSR_CAPTURE_NVFBC_H
+
+#include "capture.h"
+#include "../vec2.h"
+
+typedef struct {
+ gsr_egl *egl;
+ const char *display_to_capture; /* if this is "screen", then the entire x11 screen is captured (all displays). A copy is made of this */
+ int fps;
+ vec2i pos;
+ vec2i size;
+ bool direct_capture;
+ bool overclock;
+ bool hdr;
+ gsr_color_range color_range;
+ bool record_cursor;
+} gsr_capture_nvfbc_params;
+
+gsr_capture* gsr_capture_nvfbc_create(const gsr_capture_nvfbc_params *params);
+
+#endif /* GSR_CAPTURE_NVFBC_H */
diff --git a/include/capture/xcomposite.h b/include/capture/xcomposite.h
new file mode 100644
index 0000000..ce0dbad
--- /dev/null
+++ b/include/capture/xcomposite.h
@@ -0,0 +1,51 @@
+#ifndef GSR_CAPTURE_XCOMPOSITE_H
+#define GSR_CAPTURE_XCOMPOSITE_H
+
+#include "capture.h"
+#include "../egl.h"
+#include "../vec2.h"
+#include "../color_conversion.h"
+#include "../window_texture.h"
+#include "../cursor.h"
+
+typedef struct {
+ gsr_egl *egl;
+ Window window;
+ bool follow_focused; /* If this is set then |window| is ignored */
+ vec2i region_size; /* This is currently only used with |follow_focused| */
+ gsr_color_range color_range;
+ bool record_cursor;
+} gsr_capture_xcomposite_params;
+
+typedef struct {
+ gsr_capture_base base;
+ gsr_capture_xcomposite_params params;
+ XEvent xev;
+
+ bool should_stop;
+ bool stop_is_error;
+ bool window_resized;
+ bool follow_focused_initialized;
+
+ Window window;
+ vec2i window_size;
+ vec2i texture_size;
+ double window_resize_timer;
+
+ WindowTexture window_texture;
+
+ Atom net_active_window_atom;
+
+ gsr_cursor cursor;
+ bool clear_next_frame;
+} gsr_capture_xcomposite;
+
+void gsr_capture_xcomposite_init(gsr_capture_xcomposite *self, const gsr_capture_xcomposite_params *params);
+
+int gsr_capture_xcomposite_start(gsr_capture_xcomposite *self, AVCodecContext *video_codec_context, AVFrame *frame);
+void gsr_capture_xcomposite_stop(gsr_capture_xcomposite *self);
+void gsr_capture_xcomposite_tick(gsr_capture_xcomposite *self, AVCodecContext *video_codec_context);
+bool gsr_capture_xcomposite_should_stop(gsr_capture_xcomposite *self, bool *err);
+int gsr_capture_xcomposite_capture(gsr_capture_xcomposite *self, AVFrame *frame);
+
+#endif /* GSR_CAPTURE_XCOMPOSITE_H */
diff --git a/include/capture/xcomposite_cuda.h b/include/capture/xcomposite_cuda.h
new file mode 100644
index 0000000..b93c6de
--- /dev/null
+++ b/include/capture/xcomposite_cuda.h
@@ -0,0 +1,14 @@
+#ifndef GSR_CAPTURE_XCOMPOSITE_CUDA_H
+#define GSR_CAPTURE_XCOMPOSITE_CUDA_H
+
+#include "capture.h"
+#include "xcomposite.h"
+
+typedef struct {
+ gsr_capture_xcomposite_params base;
+ bool overclock;
+} gsr_capture_xcomposite_cuda_params;
+
+gsr_capture* gsr_capture_xcomposite_cuda_create(const gsr_capture_xcomposite_cuda_params *params);
+
+#endif /* GSR_CAPTURE_XCOMPOSITE_CUDA_H */
diff --git a/include/capture/xcomposite_vaapi.h b/include/capture/xcomposite_vaapi.h
new file mode 100644
index 0000000..5d4b338
--- /dev/null
+++ b/include/capture/xcomposite_vaapi.h
@@ -0,0 +1,13 @@
+#ifndef GSR_CAPTURE_XCOMPOSITE_VAAPI_H
+#define GSR_CAPTURE_XCOMPOSITE_VAAPI_H
+
+#include "capture.h"
+#include "xcomposite.h"
+
+typedef struct {
+ gsr_capture_xcomposite_params base;
+} gsr_capture_xcomposite_vaapi_params;
+
+gsr_capture* gsr_capture_xcomposite_vaapi_create(const gsr_capture_xcomposite_vaapi_params *params);
+
+#endif /* GSR_CAPTURE_XCOMPOSITE_VAAPI_H */
diff --git a/include/color_conversion.h b/include/color_conversion.h
new file mode 100644
index 0000000..d05df6a
--- /dev/null
+++ b/include/color_conversion.h
@@ -0,0 +1,58 @@
+#ifndef GSR_COLOR_CONVERSION_H
+#define GSR_COLOR_CONVERSION_H
+
+#include "shader.h"
+#include "vec2.h"
+#include <stdbool.h>
+
+typedef enum {
+ GSR_COLOR_RANGE_LIMITED,
+ GSR_COLOR_RANGE_FULL
+} gsr_color_range;
+
+typedef enum {
+ GSR_SOURCE_COLOR_RGB,
+ GSR_SOURCE_COLOR_BGR
+} gsr_source_color;
+
+typedef enum {
+ GSR_DESTINATION_COLOR_NV12, /* YUV420, BT709, 8-bit */
+ GSR_DESTINATION_COLOR_P010 /* YUV420, BT2020, 10-bit */
+} gsr_destination_color;
+
+typedef struct {
+ int offset;
+ int rotation;
+} gsr_color_uniforms;
+
+typedef struct {
+ gsr_egl *egl;
+
+ gsr_source_color source_color;
+ gsr_destination_color destination_color;
+
+ unsigned int destination_textures[2];
+ int num_destination_textures;
+
+ gsr_color_range color_range;
+ bool load_external_image_shader;
+} gsr_color_conversion_params;
+
+typedef struct {
+ gsr_color_conversion_params params;
+ gsr_color_uniforms uniforms[4];
+ gsr_shader shaders[4];
+
+ unsigned int framebuffers[2];
+
+ unsigned int vertex_array_object_id;
+ unsigned int vertex_buffer_object_id;
+} gsr_color_conversion;
+
+int gsr_color_conversion_init(gsr_color_conversion *self, const gsr_color_conversion_params *params);
+void gsr_color_conversion_deinit(gsr_color_conversion *self);
+
+void gsr_color_conversion_draw(gsr_color_conversion *self, unsigned int texture_id, vec2i source_pos, vec2i source_size, vec2i texture_pos, vec2i texture_size, float rotation, bool external_texture);
+void gsr_color_conversion_clear(gsr_color_conversion *self);
+
+#endif /* GSR_COLOR_CONVERSION_H */
diff --git a/include/cuda.h b/include/cuda.h
new file mode 100644
index 0000000..fd1f9f9
--- /dev/null
+++ b/include/cuda.h
@@ -0,0 +1,108 @@
+#ifndef GSR_CUDA_H
+#define GSR_CUDA_H
+
+#include "overclock.h"
+#include <stddef.h>
+#include <stdbool.h>
+
+// To prevent hwcontext_cuda.h from including cuda.h
+#define CUDA_VERSION 11070
+
+#define CU_CTX_SCHED_AUTO 0
+
+#if defined(_WIN64) || defined(__LP64__)
+typedef unsigned long long CUdeviceptr_v2;
+#else
+typedef unsigned int CUdeviceptr_v2;
+#endif
+typedef CUdeviceptr_v2 CUdeviceptr;
+
+typedef int CUresult;
+typedef int CUdevice_v1;
+typedef CUdevice_v1 CUdevice;
+typedef struct CUctx_st *CUcontext;
+typedef struct CUstream_st *CUstream;
+typedef struct CUarray_st *CUarray;
+
+#define CUDA_SUCCESS 0
+
+typedef enum CUgraphicsMapResourceFlags_enum {
+ CU_GRAPHICS_MAP_RESOURCE_FLAGS_NONE = 0x00,
+ CU_GRAPHICS_MAP_RESOURCE_FLAGS_READ_ONLY = 0x01,
+ CU_GRAPHICS_MAP_RESOURCE_FLAGS_WRITE_DISCARD = 0x02
+} CUgraphicsMapResourceFlags;
+
+typedef enum CUgraphicsRegisterFlags_enum {
+ CU_GRAPHICS_REGISTER_FLAGS_NONE = 0x00,
+ CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY = 0x01,
+ CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD = 0x02,
+ CU_GRAPHICS_REGISTER_FLAGS_SURFACE_LDST = 0x04,
+ CU_GRAPHICS_REGISTER_FLAGS_TEXTURE_GATHER = 0x08
+} CUgraphicsRegisterFlags;
+
+typedef enum CUmemorytype_enum {
+ CU_MEMORYTYPE_HOST = 0x01, /**< Host memory */
+ CU_MEMORYTYPE_DEVICE = 0x02, /**< Device memory */
+ CU_MEMORYTYPE_ARRAY = 0x03, /**< Array memory */
+ CU_MEMORYTYPE_UNIFIED = 0x04 /**< Unified device or host memory */
+} CUmemorytype;
+
+typedef struct CUDA_MEMCPY2D_st {
+ size_t srcXInBytes; /**< Source X in bytes */
+ size_t srcY; /**< Source Y */
+
+ CUmemorytype srcMemoryType; /**< Source memory type (host, device, array) */
+ const void *srcHost; /**< Source host pointer */
+ CUdeviceptr srcDevice; /**< Source device pointer */
+ CUarray srcArray; /**< Source array reference */
+ size_t srcPitch; /**< Source pitch (ignored when src is array) */
+
+ size_t dstXInBytes; /**< Destination X in bytes */
+ size_t dstY; /**< Destination Y */
+
+ CUmemorytype dstMemoryType; /**< Destination memory type (host, device, array) */
+ void *dstHost; /**< Destination host pointer */
+ CUdeviceptr dstDevice; /**< Destination device pointer */
+ CUarray dstArray; /**< Destination array reference */
+ size_t dstPitch; /**< Destination pitch (ignored when dst is array) */
+
+ size_t WidthInBytes; /**< Width of 2D memory copy in bytes */
+ size_t Height; /**< Height of 2D memory copy */
+} CUDA_MEMCPY2D_v2;
+typedef CUDA_MEMCPY2D_v2 CUDA_MEMCPY2D;
+
+typedef struct CUgraphicsResource_st *CUgraphicsResource;
+
+typedef struct gsr_cuda gsr_cuda;
+struct gsr_cuda {
+ gsr_overclock overclock;
+ bool do_overclock;
+
+ void *library;
+ CUcontext cu_ctx;
+
+ CUresult (*cuInit)(unsigned int Flags);
+ CUresult (*cuDeviceGetCount)(int *count);
+ CUresult (*cuDeviceGet)(CUdevice *device, int ordinal);
+ CUresult (*cuCtxCreate_v2)(CUcontext *pctx, unsigned int flags, CUdevice dev);
+ CUresult (*cuCtxDestroy_v2)(CUcontext ctx);
+ CUresult (*cuCtxPushCurrent_v2)(CUcontext ctx);
+ CUresult (*cuCtxPopCurrent_v2)(CUcontext *pctx);
+ CUresult (*cuGetErrorString)(CUresult error, const char **pStr);
+ CUresult (*cuMemcpy2D_v2)(const CUDA_MEMCPY2D *pCopy);
+ CUresult (*cuMemcpy2DAsync_v2)(const CUDA_MEMCPY2D *pcopy, CUstream hStream);
+ CUresult (*cuStreamSynchronize)(CUstream hStream);
+
+ CUresult (*cuGraphicsGLRegisterImage)(CUgraphicsResource *pCudaResource, unsigned int image, unsigned int target, unsigned int flags);
+ CUresult (*cuGraphicsEGLRegisterImage)(CUgraphicsResource *pCudaResource, void *image, unsigned int flags);
+ CUresult (*cuGraphicsResourceSetMapFlags)(CUgraphicsResource resource, unsigned int flags);
+ CUresult (*cuGraphicsMapResources)(unsigned int count, CUgraphicsResource *resources, CUstream hStream);
+ CUresult (*cuGraphicsUnmapResources)(unsigned int count, CUgraphicsResource *resources, CUstream hStream);
+ CUresult (*cuGraphicsUnregisterResource)(CUgraphicsResource resource);
+ CUresult (*cuGraphicsSubResourceGetMappedArray)(CUarray *pArray, CUgraphicsResource resource, unsigned int arrayIndex, unsigned int mipLevel);
+};
+
+bool gsr_cuda_load(gsr_cuda *self, Display *display, bool overclock);
+void gsr_cuda_unload(gsr_cuda *self);
+
+#endif /* GSR_CUDA_H */
diff --git a/include/cursor.h b/include/cursor.h
new file mode 100644
index 0000000..b1ec6bd
--- /dev/null
+++ b/include/cursor.h
@@ -0,0 +1,26 @@
+#ifndef GSR_CURSOR_H
+#define GSR_CURSOR_H
+
+#include "egl.h"
+#include "vec2.h"
+
+typedef struct {
+ gsr_egl *egl;
+ Display *display;
+ int x_fixes_event_base;
+
+ unsigned int texture_id;
+ vec2i size;
+ vec2i hotspot;
+ vec2i position;
+
+ bool cursor_image_set;
+} gsr_cursor;
+
+int gsr_cursor_init(gsr_cursor *self, gsr_egl *egl, Display *display);
+void gsr_cursor_deinit(gsr_cursor *self);
+
+void gsr_cursor_update(gsr_cursor *self, XEvent *xev);
+void gsr_cursor_tick(gsr_cursor *self, Window relative_to);
+
+#endif /* GSR_CURSOR_H */
diff --git a/include/defs.h b/include/defs.h
new file mode 100644
index 0000000..473583c
--- /dev/null
+++ b/include/defs.h
@@ -0,0 +1,28 @@
+#ifndef GSR_DEFS_H
+#define GSR_DEFS_H
+
+typedef enum {
+ GSR_GPU_VENDOR_AMD,
+ GSR_GPU_VENDOR_INTEL,
+ GSR_GPU_VENDOR_NVIDIA
+} gsr_gpu_vendor;
+
+typedef struct {
+ gsr_gpu_vendor vendor;
+ int gpu_version; /* 0 if unknown */
+} gsr_gpu_info;
+
+typedef enum {
+ GSR_MONITOR_ROT_0,
+ GSR_MONITOR_ROT_90,
+ GSR_MONITOR_ROT_180,
+ GSR_MONITOR_ROT_270
+} gsr_monitor_rotation;
+
+typedef enum {
+ GSR_CONNECTION_X11,
+ GSR_CONNECTION_WAYLAND,
+ GSR_CONNECTION_DRM
+} gsr_connection_type;
+
+#endif /* GSR_DEFS_H */
diff --git a/include/egl.h b/include/egl.h
new file mode 100644
index 0000000..afdb5a9
--- /dev/null
+++ b/include/egl.h
@@ -0,0 +1,285 @@
+#ifndef GSR_EGL_H
+#define GSR_EGL_H
+
+/* OpenGL EGL library with a hidden window context (to allow using the opengl functions) */
+
+#include <X11/X.h>
+#include <X11/Xutil.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include "vec2.h"
+#include "defs.h"
+
+#ifdef _WIN64
+typedef signed long long int khronos_intptr_t;
+typedef unsigned long long int khronos_uintptr_t;
+typedef signed long long int khronos_ssize_t;
+typedef unsigned long long int khronos_usize_t;
+#else
+typedef signed long int khronos_intptr_t;
+typedef unsigned long int khronos_uintptr_t;
+typedef signed long int khronos_ssize_t;
+typedef unsigned long int khronos_usize_t;
+#endif
+
+typedef void* EGLDisplay;
+typedef void* EGLNativeDisplayType;
+typedef uintptr_t EGLNativeWindowType;
+typedef uintptr_t EGLNativePixmapType;
+typedef void* EGLConfig;
+typedef void* EGLSurface;
+typedef void* EGLContext;
+typedef void* EGLClientBuffer;
+typedef void* EGLImage;
+typedef void* EGLImageKHR;
+typedef void *GLeglImageOES;
+typedef void (*__eglMustCastToProperFunctionPointerType)(void);
+typedef struct __GLXFBConfigRec *GLXFBConfig;
+typedef struct __GLXcontextRec *GLXContext;
+typedef XID GLXDrawable;
+typedef void(*__GLXextFuncPtr)(void);
+
+#define EGL_SUCCESS 0x3000
+#define EGL_BUFFER_SIZE 0x3020
+#define EGL_RENDERABLE_TYPE 0x3040
+#define EGL_OPENGL_API 0x30A2
+#define EGL_OPENGL_BIT 0x0008
+#define EGL_NONE 0x3038
+#define EGL_CONTEXT_CLIENT_VERSION 0x3098
+#define EGL_BACK_BUFFER 0x3084
+#define EGL_GL_TEXTURE_2D 0x30B1
+#define EGL_TRUE 1
+#define EGL_IMAGE_PRESERVED_KHR 0x30D2
+#define EGL_NATIVE_PIXMAP_KHR 0x30B0
+#define EGL_LINUX_DRM_FOURCC_EXT 0x3271
+#define EGL_WIDTH 0x3057
+#define EGL_HEIGHT 0x3056
+#define EGL_DMA_BUF_PLANE0_FD_EXT 0x3272
+#define EGL_DMA_BUF_PLANE0_OFFSET_EXT 0x3273
+#define EGL_DMA_BUF_PLANE0_PITCH_EXT 0x3274
+#define EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT 0x3443
+#define EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT 0x3444
+#define EGL_LINUX_DMA_BUF_EXT 0x3270
+#define EGL_RED_SIZE 0x3024
+#define EGL_ALPHA_SIZE 0x3021
+#define EGL_BLUE_SIZE 0x3022
+#define EGL_GREEN_SIZE 0x3023
+#define EGL_CONTEXT_PRIORITY_LEVEL_IMG 0x3100
+#define EGL_CONTEXT_PRIORITY_HIGH_IMG 0x3101
+#define EGL_CONTEXT_PRIORITY_MEDIUM_IMG 0x3102
+#define EGL_CONTEXT_PRIORITY_LOW_IMG 0x3103
+#define EGL_DEVICE_EXT 0x322C
+#define EGL_DRM_DEVICE_FILE_EXT 0x3233
+
+#define GL_FLOAT 0x1406
+#define GL_FALSE 0
+#define GL_TRUE 1
+#define GL_TRIANGLES 0x0004
+#define GL_TEXTURE_2D 0x0DE1
+#define GL_TEXTURE_EXTERNAL_OES 0x8D65
+#define GL_RED 0x1903
+#define GL_GREEN 0x1904
+#define GL_BLUE 0x1905
+#define GL_ALPHA 0x1906
+#define GL_TEXTURE_SWIZZLE_RGBA 0x8E46
+#define GL_RG 0x8227
+#define GL_RGB 0x1907
+#define GL_RGBA 0x1908
+#define GL_RGBA8 0x8058
+#define GL_R8 0x8229
+#define GL_RG8 0x822B
+#define GL_R16 0x822A
+#define GL_RG16 0x822C
+#define GL_UNSIGNED_BYTE 0x1401
+#define GL_COLOR_BUFFER_BIT 0x00004000
+#define GL_TEXTURE_WRAP_S 0x2802
+#define GL_TEXTURE_WRAP_T 0x2803
+#define GL_TEXTURE_MAG_FILTER 0x2800
+#define GL_TEXTURE_MIN_FILTER 0x2801
+#define GL_TEXTURE_WIDTH 0x1000
+#define GL_TEXTURE_HEIGHT 0x1001
+#define GL_NEAREST 0x2600
+#define GL_CLAMP_TO_EDGE 0x812F
+#define GL_LINEAR 0x2601
+#define GL_FRAMEBUFFER 0x8D40
+#define GL_COLOR_ATTACHMENT0 0x8CE0
+#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
+#define GL_STREAM_DRAW 0x88E0
+#define GL_ARRAY_BUFFER 0x8892
+#define GL_BLEND 0x0BE2
+#define GL_SRC_ALPHA 0x0302
+#define GL_ONE_MINUS_SRC_ALPHA 0x0303
+#define GL_DEBUG_OUTPUT 0x92E0
+
+#define GL_VENDOR 0x1F00
+#define GL_RENDERER 0x1F01
+
+#define GL_COMPILE_STATUS 0x8B81
+#define GL_INFO_LOG_LENGTH 0x8B84
+#define GL_FRAGMENT_SHADER 0x8B30
+#define GL_VERTEX_SHADER 0x8B31
+#define GL_COMPILE_STATUS 0x8B81
+#define GL_LINK_STATUS 0x8B82
+
+typedef unsigned int (*FUNC_eglExportDMABUFImageQueryMESA)(EGLDisplay dpy, EGLImageKHR image, int *fourcc, int *num_planes, uint64_t *modifiers);
+typedef unsigned int (*FUNC_eglExportDMABUFImageMESA)(EGLDisplay dpy, EGLImageKHR image, int *fds, int32_t *strides, int32_t *offsets);
+typedef void (*FUNC_glEGLImageTargetTexture2DOES)(unsigned int target, GLeglImageOES image);
+typedef GLXContext (*FUNC_glXCreateContextAttribsARB)(Display *dpy, GLXFBConfig config, GLXContext share_context, Bool direct, const int *attrib_list);
+typedef void (*FUNC_glXSwapIntervalEXT)(Display * dpy, GLXDrawable drawable, int interval);
+typedef int (*FUNC_glXSwapIntervalMESA)(unsigned int interval);
+typedef int (*FUNC_glXSwapIntervalSGI)(int interval);
+typedef void (*GLDEBUGPROC)(unsigned int source, unsigned int type, unsigned int id, unsigned int severity, int length, const char *message, const void *userParam);
+typedef int (*FUNC_eglQueryDisplayAttribEXT)(EGLDisplay dpy, int32_t attribute, intptr_t *value);
+typedef const char* (*FUNC_eglQueryDeviceStringEXT)(void *device, int32_t name);
+
+#define GSR_MAX_OUTPUTS 32
+
+typedef struct {
+ Display *dpy;
+ Window window;
+} gsr_x11;
+
+typedef struct {
+ uint32_t wl_name;
+ void *output;
+ vec2i pos;
+ vec2i size;
+ int32_t transform;
+ char *name;
+} gsr_wayland_output;
+
+typedef struct {
+ void *dpy;
+ void *window;
+ void *registry;
+ void *surface;
+ void *compositor;
+ gsr_wayland_output outputs[GSR_MAX_OUTPUTS];
+ int num_outputs;
+} gsr_wayland;
+
+typedef enum {
+ GSR_GL_CONTEXT_TYPE_EGL,
+ GSR_GL_CONTEXT_TYPE_GLX
+} gsr_gl_context_type;
+
+typedef struct gsr_egl gsr_egl;
+struct gsr_egl {
+ void *egl_library;
+ void *glx_library;
+ void *gl_library;
+
+ gsr_gl_context_type context_type;
+
+ EGLDisplay egl_display;
+ EGLSurface egl_surface;
+ EGLContext egl_context;
+ const char *dri_card_path;
+
+ void *glx_context;
+ void *glx_fb_config;
+
+ gsr_gpu_info gpu_info;
+
+ gsr_x11 x11;
+ gsr_wayland wayland;
+ char card_path[128];
+
+ int32_t (*eglGetError)(void);
+ EGLDisplay (*eglGetDisplay)(EGLNativeDisplayType display_id);
+ unsigned int (*eglInitialize)(EGLDisplay dpy, int32_t *major, int32_t *minor);
+ unsigned int (*eglTerminate)(EGLDisplay dpy);
+ unsigned int (*eglChooseConfig)(EGLDisplay dpy, const int32_t *attrib_list, EGLConfig *configs, int32_t config_size, int32_t *num_config);
+ EGLSurface (*eglCreateWindowSurface)(EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const int32_t *attrib_list);
+ EGLContext (*eglCreateContext)(EGLDisplay dpy, EGLConfig config, EGLContext share_context, const int32_t *attrib_list);
+ unsigned int (*eglMakeCurrent)(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
+ EGLImage (*eglCreateImage)(EGLDisplay dpy, EGLContext ctx, unsigned int target, EGLClientBuffer buffer, const intptr_t *attrib_list);
+ unsigned int (*eglDestroyContext)(EGLDisplay dpy, EGLContext ctx);
+ unsigned int (*eglDestroySurface)(EGLDisplay dpy, EGLSurface surface);
+ unsigned int (*eglDestroyImage)(EGLDisplay dpy, EGLImage image);
+ unsigned int (*eglSwapInterval)(EGLDisplay dpy, int32_t interval);
+ unsigned int (*eglSwapBuffers)(EGLDisplay dpy, EGLSurface surface);
+ unsigned int (*eglBindAPI)(unsigned int api);
+ __eglMustCastToProperFunctionPointerType (*eglGetProcAddress)(const char *procname);
+
+ FUNC_eglExportDMABUFImageQueryMESA eglExportDMABUFImageQueryMESA;
+ FUNC_eglExportDMABUFImageMESA eglExportDMABUFImageMESA;
+ FUNC_glEGLImageTargetTexture2DOES glEGLImageTargetTexture2DOES;
+ FUNC_eglQueryDisplayAttribEXT eglQueryDisplayAttribEXT;
+ FUNC_eglQueryDeviceStringEXT eglQueryDeviceStringEXT;
+
+ __GLXextFuncPtr (*glXGetProcAddress)(const unsigned char *procName);
+ GLXFBConfig* (*glXChooseFBConfig)(Display *dpy, int screen, const int *attribList, int *nitems);
+ Bool (*glXMakeContextCurrent)(Display *dpy, GLXDrawable draw, GLXDrawable read, GLXContext ctx);
+ // TODO: Remove
+ GLXContext (*glXCreateNewContext)(Display *dpy, GLXFBConfig config, int renderType, GLXContext shareList, Bool direct);
+ void (*glXDestroyContext)(Display *dpy, GLXContext ctx);
+ void (*glXSwapBuffers)(Display *dpy, GLXDrawable drawable);
+ FUNC_glXCreateContextAttribsARB glXCreateContextAttribsARB;
+
+ /* Optional */
+ FUNC_glXSwapIntervalEXT glXSwapIntervalEXT;
+ FUNC_glXSwapIntervalMESA glXSwapIntervalMESA;
+ FUNC_glXSwapIntervalSGI glXSwapIntervalSGI;
+
+ unsigned int (*glGetError)(void);
+ const unsigned char* (*glGetString)(unsigned int name);
+ void (*glFlush)(void);
+ void (*glFinish)(void);
+ void (*glClear)(unsigned int mask);
+ void (*glClearColor)(float red, float green, float blue, float alpha);
+ void (*glGenTextures)(int n, unsigned int *textures);
+ void (*glDeleteTextures)(int n, const unsigned int *texture);
+ void (*glBindTexture)(unsigned int target, unsigned int texture);
+ void (*glTexParameteri)(unsigned int target, unsigned int pname, int param);
+ void (*glTexParameteriv)(unsigned int target, unsigned int pname, const int *params);
+ void (*glGetTexLevelParameteriv)(unsigned int target, int level, unsigned int pname, int *params);
+ void (*glTexImage2D)(unsigned int target, int level, int internalFormat, int width, int height, int border, unsigned int format, unsigned int type, const void *pixels);
+ void (*glCopyImageSubData)(unsigned int srcName, unsigned int srcTarget, int srcLevel, int srcX, int srcY, int srcZ, unsigned int dstName, unsigned int dstTarget, int dstLevel, int dstX, int dstY, int dstZ, int srcWidth, int srcHeight, int srcDepth);
+ void (*glClearTexImage)(unsigned int texture, unsigned int level, unsigned int format, unsigned int type, const void *data);
+ void (*glGenFramebuffers)(int n, unsigned int *framebuffers);
+ void (*glBindFramebuffer)(unsigned int target, unsigned int framebuffer);
+ void (*glDeleteFramebuffers)(int n, const unsigned int *framebuffers);
+ void (*glViewport)(int x, int y, int width, int height);
+ void (*glFramebufferTexture2D)(unsigned int target, unsigned int attachment, unsigned int textarget, unsigned int texture, int level);
+ void (*glDrawBuffers)(int n, const unsigned int *bufs);
+ unsigned int (*glCheckFramebufferStatus)(unsigned int target);
+ void (*glBindBuffer)(unsigned int target, unsigned int buffer);
+ void (*glGenBuffers)(int n, unsigned int *buffers);
+ void (*glBufferData)(unsigned int target, khronos_ssize_t size, const void *data, unsigned int usage);
+ void (*glBufferSubData)(unsigned int target, khronos_intptr_t offset, khronos_ssize_t size, const void *data);
+ void (*glDeleteBuffers)(int n, const unsigned int *buffers);
+ void (*glGenVertexArrays)(int n, unsigned int *arrays);
+ void (*glBindVertexArray)(unsigned int array);
+ void (*glDeleteVertexArrays)(int n, const unsigned int *arrays);
+ unsigned int (*glCreateProgram)(void);
+ unsigned int (*glCreateShader)(unsigned int type);
+ void (*glAttachShader)(unsigned int program, unsigned int shader);
+ void (*glBindAttribLocation)(unsigned int program, unsigned int index, const char *name);
+ void (*glCompileShader)(unsigned int shader);
+ void (*glLinkProgram)(unsigned int program);
+ void (*glShaderSource)(unsigned int shader, int count, const char *const*string, const int *length);
+ void (*glUseProgram)(unsigned int program);
+ void (*glGetProgramInfoLog)(unsigned int program, int bufSize, int *length, char *infoLog);
+ void (*glGetShaderiv)(unsigned int shader, unsigned int pname, int *params);
+ void (*glGetShaderInfoLog)(unsigned int shader, int bufSize, int *length, char *infoLog);
+ void (*glDeleteProgram)(unsigned int program);
+ void (*glDeleteShader)(unsigned int shader);
+ void (*glGetProgramiv)(unsigned int program, unsigned int pname, int *params);
+ void (*glVertexAttribPointer)(unsigned int index, int size, unsigned int type, unsigned char normalized, int stride, const void *pointer);
+ void (*glEnableVertexAttribArray)(unsigned int index);
+ void (*glDrawArrays)(unsigned int mode, int first, int count);
+ void (*glEnable)(unsigned int cap);
+ void (*glBlendFunc)(unsigned int sfactor, unsigned int dfactor);
+ int (*glGetUniformLocation)(unsigned int program, const char *name);
+ void (*glUniform1f)(int location, float v0);
+ void (*glUniform2f)(int location, float v0, float v1);
+ void (*glDebugMessageCallback)(GLDEBUGPROC callback, const void *userParam);
+};
+
+bool gsr_egl_load(gsr_egl *self, Display *dpy, bool wayland, bool is_monitor_capture);
+void gsr_egl_unload(gsr_egl *self);
+
+void gsr_egl_update(gsr_egl *self);
+
+#endif /* GSR_EGL_H */
diff --git a/include/library_loader.h b/include/library_loader.h
new file mode 100644
index 0000000..47bc9f0
--- /dev/null
+++ b/include/library_loader.h
@@ -0,0 +1,17 @@
+#ifndef GSR_LIBRARY_LOADER_H
+#define GSR_LIBRARY_LOADER_H
+
+#include <stdbool.h>
+
+typedef struct {
+ void **func;
+ const char *name;
+} dlsym_assign;
+
+void* dlsym_print_fail(void *handle, const char *name, bool required);
+/* |dlsyms| should be null terminated */
+bool dlsym_load_list(void *handle, const dlsym_assign *dlsyms);
+/* |dlsyms| should be null terminated */
+void dlsym_load_list_optional(void *handle, const dlsym_assign *dlsyms);
+
+#endif /* GSR_LIBRARY_LOADER_H */
diff --git a/include/overclock.h b/include/overclock.h
new file mode 100644
index 0000000..d6ff901
--- /dev/null
+++ b/include/overclock.h
@@ -0,0 +1,17 @@
+#ifndef GSR_OVERCLOCK_H
+#define GSR_OVERCLOCK_H
+
+#include "xnvctrl.h"
+
+typedef struct {
+ gsr_xnvctrl xnvctrl;
+ int num_performance_levels;
+} gsr_overclock;
+
+bool gsr_overclock_load(gsr_overclock *self, Display *display);
+void gsr_overclock_unload(gsr_overclock *self);
+
+bool gsr_overclock_start(gsr_overclock *self);
+void gsr_overclock_stop(gsr_overclock *self);
+
+#endif /* GSR_OVERCLOCK_H */
diff --git a/include/shader.h b/include/shader.h
new file mode 100644
index 0000000..57d1096
--- /dev/null
+++ b/include/shader.h
@@ -0,0 +1,19 @@
+#ifndef GSR_SHADER_H
+#define GSR_SHADER_H
+
+typedef struct gsr_egl gsr_egl;
+
+typedef struct {
+ gsr_egl *egl;
+ unsigned int program_id;
+} gsr_shader;
+
+/* |vertex_shader| or |fragment_shader| may be NULL */
+int gsr_shader_init(gsr_shader *self, gsr_egl *egl, const char *vertex_shader, const char *fragment_shader);
+void gsr_shader_deinit(gsr_shader *self);
+
+int gsr_shader_bind_attribute_location(gsr_shader *self, const char *attribute, int location);
+void gsr_shader_use(gsr_shader *self);
+void gsr_shader_use_none(gsr_shader *self);
+
+#endif /* GSR_SHADER_H */
diff --git a/include/sound.hpp b/include/sound.hpp
index 666d009..77bec99 100644
--- a/include/sound.hpp
+++ b/include/sound.hpp
@@ -18,20 +18,36 @@
#ifndef GPU_SCREEN_RECORDER_H
#define GPU_SCREEN_RECORDER_H
+#include <vector>
+#include <string>
+
typedef struct {
void *handle;
- void *buffer;
- int buffer_size;
unsigned int frames;
} SoundDevice;
+struct AudioInput {
+ std::string name;
+ std::string description;
+};
+
+struct MergedAudioInputs {
+ std::vector<AudioInput> audio_inputs;
+};
+
+typedef enum {
+ S16,
+ S32,
+ F32
+} AudioFormat;
+
/*
Get a sound device by name, returning the device into the @device parameter.
The device should be closed with @sound_device_close after it has been used
to clean up internal resources.
Returns 0 on success, or a negative value on failure.
*/
-int sound_device_get_by_name(SoundDevice *device, const char *name = "default", unsigned int num_channels = 2, unsigned int period_frame_size = 32);
+int sound_device_get_by_name(SoundDevice *device, const char *device_name, const char *description, unsigned int num_channels, unsigned int period_frame_size, AudioFormat audio_format);
void sound_device_close(SoundDevice *device);
@@ -39,6 +55,8 @@ void sound_device_close(SoundDevice *device);
Returns the next chunk of audio into @buffer.
Returns the number of frames read, or a negative value on failure.
*/
-int sound_device_read_next_chunk(SoundDevice *device, void **buffer);
+int sound_device_read_next_chunk(SoundDevice *device, void **buffer, double timeout_sec, double *latency_seconds);
+
+std::vector<AudioInput> get_pulseaudio_inputs();
#endif /* GPU_SCREEN_RECORDER_H */
diff --git a/include/utils.h b/include/utils.h
new file mode 100644
index 0000000..74fdd59
--- /dev/null
+++ b/include/utils.h
@@ -0,0 +1,46 @@
+#ifndef GSR_UTILS_H
+#define GSR_UTILS_H
+
+#include "vec2.h"
+#include "../include/egl.h"
+#include "../include/defs.h"
+#include <stdbool.h>
+#include <stdint.h>
+#include <X11/extensions/Xrandr.h>
+
+typedef struct {
+ const char *name;
+ int name_len;
+ vec2i pos;
+ vec2i size;
+ XRRCrtcInfo *crt_info; /* Only on x11 */
+ uint32_t connector_id; /* Only on x11 and drm */
+ gsr_monitor_rotation rotation; /* Only on x11 and wayland */
+ uint32_t monitor_identifier; /* Only on drm and wayland */
+} gsr_monitor;
+
+typedef struct {
+ const char *name;
+ int name_len;
+ gsr_monitor *monitor;
+ bool found_monitor;
+} get_monitor_by_name_userdata;
+
+double clock_get_monotonic_seconds(void);
+
+typedef void (*active_monitor_callback)(const gsr_monitor *monitor, void *userdata);
+void for_each_active_monitor_output_x11(Display *display, active_monitor_callback callback, void *userdata);
+void for_each_active_monitor_output(const gsr_egl *egl, gsr_connection_type connection_type, active_monitor_callback callback, void *userdata);
+bool get_monitor_by_name(const gsr_egl *egl, gsr_connection_type connection_type, const char *name, gsr_monitor *monitor);
+gsr_monitor_rotation drm_monitor_get_display_server_rotation(const gsr_egl *egl, const gsr_monitor *monitor);
+
+bool gl_get_gpu_info(gsr_egl *egl, gsr_gpu_info *info);
+
+/* |output| should be at least 128 bytes in size */
+bool gsr_get_valid_card_path(gsr_egl *egl, char *output);
+/* |render_path| should be at least 128 bytes in size */
+bool gsr_card_path_get_render_path(const char *card_path, char *render_path);
+
+int even_number_ceil(int value);
+
+#endif /* GSR_UTILS_H */
diff --git a/include/vec2.h b/include/vec2.h
new file mode 100644
index 0000000..3e33cfb
--- /dev/null
+++ b/include/vec2.h
@@ -0,0 +1,12 @@
+#ifndef VEC2_H
+#define VEC2_H
+
+typedef struct {
+ int x, y;
+} vec2i;
+
+typedef struct {
+ float x, y;
+} vec2f;
+
+#endif /* VEC2_H */
diff --git a/include/window_texture.h b/include/window_texture.h
new file mode 100644
index 0000000..75bb2a7
--- /dev/null
+++ b/include/window_texture.h
@@ -0,0 +1,27 @@
+#ifndef WINDOW_TEXTURE_H
+#define WINDOW_TEXTURE_H
+
+#include "egl.h"
+
+typedef struct {
+ Display *display;
+ Window window;
+ Pixmap pixmap;
+ unsigned int texture_id;
+ int redirected;
+ gsr_egl *egl;
+} WindowTexture;
+
+/* Returns 0 on success */
+int window_texture_init(WindowTexture *window_texture, Display *display, Window window, gsr_egl *egl);
+void window_texture_deinit(WindowTexture *self);
+
+/*
+ This should ONLY be called when the target window is resized.
+ Returns 0 on success.
+*/
+int window_texture_on_resize(WindowTexture *self);
+
+unsigned int window_texture_get_opengl_texture_id(WindowTexture *self);
+
+#endif /* WINDOW_TEXTURE_H */
diff --git a/include/xnvctrl.h b/include/xnvctrl.h
new file mode 100644
index 0000000..33fc442
--- /dev/null
+++ b/include/xnvctrl.h
@@ -0,0 +1,45 @@
+#ifndef GSR_XNVCTRL_H
+#define GSR_XNVCTRL_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#define NV_CTRL_GPU_NVCLOCK_OFFSET 409
+#define NV_CTRL_GPU_MEM_TRANSFER_RATE_OFFSET 410
+#define NV_CTRL_GPU_NVCLOCK_OFFSET_ALL_PERFORMANCE_LEVELS 424
+#define NV_CTRL_GPU_MEM_TRANSFER_RATE_OFFSET_ALL_PERFORMANCE_LEVELS 425
+
+#define NV_CTRL_TARGET_TYPE_GPU 1
+
+#define NV_CTRL_STRING_PERFORMANCE_MODES 29
+
+typedef struct _XDisplay Display;
+
+typedef struct {
+ int type;
+ union {
+ struct {
+ int64_t min;
+ int64_t max;
+ } range;
+ struct {
+ unsigned int ints;
+ } bits;
+ } u;
+ unsigned int permissions;
+} NVCTRLAttributeValidValuesRec;
+
+typedef struct {
+ Display *display;
+ void *library;
+
+ int (*XNVCTRLQueryExtension)(Display *dpy, int *event_basep, int *error_basep);
+ int (*XNVCTRLSetTargetAttributeAndGetStatus)(Display *dpy, int target_type, int target_id, unsigned int display_mask, unsigned int attribute, int value);
+ int (*XNVCTRLQueryValidTargetAttributeValues)(Display *dpy, int target_type, int target_id, unsigned int display_mask, unsigned int attribute, NVCTRLAttributeValidValuesRec *values);
+ int (*XNVCTRLQueryTargetStringAttribute)(Display *dpy, int target_type, int target_id, unsigned int display_mask, unsigned int attribute, char **ptr);
+} gsr_xnvctrl;
+
+bool gsr_xnvctrl_load(gsr_xnvctrl *self, Display *display);
+void gsr_xnvctrl_unload(gsr_xnvctrl *self);
+
+#endif /* GSR_XNVCTRL_H */
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..0833307
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,22 @@
+#!/bin/sh -e
+
+script_dir=$(dirname "$0")
+cd "$script_dir"
+
+[ $(id -u) -ne 0 ] && echo "You need root privileges to run the install script" && exit 1
+
+./build.sh
+strip gsr-kms-server
+strip gpu-screen-recorder
+
+install -Dm755 "gsr-kms-server" "/usr/bin/gsr-kms-server"
+install -Dm755 "gpu-screen-recorder" "/usr/bin/gpu-screen-recorder"
+if [ -d "/usr/lib/systemd/user" ]; then
+ install -Dm644 "extra/gpu-screen-recorder.service" "/usr/lib/systemd/user/gpu-screen-recorder.service"
+fi
+# Not necessary, but removes the password prompt when trying to record a monitor on amd/intel or nvidia wayland
+setcap cap_sys_admin+ep /usr/bin/gsr-kms-server
+# Not necessary, but allows use of EGL_CONTEXT_PRIORITY_LEVEL_IMG which allows gpu screen recorder to run without being limited to game fps under heavy load on AMD/Intel
+setcap cap_sys_nice+ep /usr/bin/gpu-screen-recorder
+
+echo "Successfully installed gpu-screen-recorder"
diff --git a/install_ubuntu.sh b/install_ubuntu.sh
deleted file mode 100755
index e443c4c..0000000
--- a/install_ubuntu.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/bin/sh
-
-script_dir=$(dirname "$0")
-cd "$script_dir"
-
-[ $(id -u) -ne 0 ] && echo "You need root privileges to run the install script" && exit 1
-
-dpkg -l nvidia-cuda-dev > /dev/null 2>&1
-cuda_missing="$?"
-
-set -e
-apt-get -y install build-essential nvidia-cuda-dev\
- libswresample-dev libavformat-dev libavcodec-dev libavutil-dev\
- libx11-dev libxcomposite-dev\
- libglew-dev libglfw3-dev\
- libpulse-dev
-
-dependencies="glew libavcodec libavformat libavutil x11 xcomposite glfw3 libpulse-simple libswresample"
-includes="$(pkg-config --cflags $dependencies) -I/opt/cuda/targets/x86_64-linux/include"
-libs="$(pkg-config --libs $dependencies) /usr/lib/x86_64-linux-gnu/stubs/libcuda.so -ldl -pthread -lm"
-g++ -c src/sound.cpp -O2 $includes -DPULSEAUDIO=1
-g++ -c src/main.cpp -O2 $includes -DPULSEAUDIO=1
-g++ -o gpu-screen-recorder -O2 sound.o main.o -s $libs
-install -Dm755 "gpu-screen-recorder" "/usr/local/bin/gpu-screen-recorder"
-
-echo "Successfully installed gpu-screen-recorder"
-[ "$cuda_missing" -eq 1 ] && echo "You need to reboot your computer before using gpu-screen-recorder because cuda was installed"
diff --git a/kms/client/kms_client.c b/kms/client/kms_client.c
new file mode 100644
index 0000000..57c6ccf
--- /dev/null
+++ b/kms/client/kms_client.c
@@ -0,0 +1,426 @@
+#include "kms_client.h"
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <unistd.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/wait.h>
+#include <sys/stat.h>
+#include <sys/capability.h>
+
+#define GSR_SOCKET_PAIR_LOCAL 0
+#define GSR_SOCKET_PAIR_REMOTE 1
+
+static void cleanup_socket(gsr_kms_client *self, bool kill_server);
+static int gsr_kms_client_replace_connection(gsr_kms_client *self);
+
+static bool generate_random_characters(char *buffer, int buffer_size, const char *alphabet, size_t alphabet_size) {
+ int fd = open("/dev/urandom", O_RDONLY);
+ if(fd == -1) {
+ perror("/dev/urandom");
+ return false;
+ }
+
+ if(read(fd, buffer, buffer_size) < buffer_size) {
+ fprintf(stderr, "Failed to read %d bytes from /dev/urandom\n", buffer_size);
+ close(fd);
+ return false;
+ }
+
+ for(int i = 0; i < buffer_size; ++i) {
+ unsigned char c = *(unsigned char*)&buffer[i];
+ buffer[i] = alphabet[c % alphabet_size];
+ }
+
+ close(fd);
+ return true;
+}
+
+static void close_fds(gsr_kms_response *response) {
+ for(int i = 0; i < response->num_fds; ++i) {
+ if(response->fds[i].fd > 0)
+ close(response->fds[i].fd);
+ response->fds[i].fd = 0;
+ }
+}
+
+static int send_msg_to_server(int server_fd, gsr_kms_request *request) {
+ struct iovec iov;
+ iov.iov_base = request;
+ iov.iov_len = sizeof(*request);
+
+ struct msghdr response_message = {0};
+ response_message.msg_iov = &iov;
+ response_message.msg_iovlen = 1;
+
+ char cmsgbuf[CMSG_SPACE(sizeof(int) * 1)];
+ memset(cmsgbuf, 0, sizeof(cmsgbuf));
+
+ if(request->new_connection_fd > 0) {
+ response_message.msg_control = cmsgbuf;
+ response_message.msg_controllen = sizeof(cmsgbuf);
+
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&response_message);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(int) * 1);
+
+ int *fds = (int*)CMSG_DATA(cmsg);
+ fds[0] = request->new_connection_fd;
+
+ response_message.msg_controllen = cmsg->cmsg_len;
+ }
+
+ return sendmsg(server_fd, &response_message, 0);
+}
+
+static int recv_msg_from_server(int server_fd, gsr_kms_response *response) {
+ struct iovec iov;
+ iov.iov_base = response;
+ iov.iov_len = sizeof(*response);
+
+ struct msghdr response_message = {0};
+ response_message.msg_iov = &iov;
+ response_message.msg_iovlen = 1;
+
+ char cmsgbuf[CMSG_SPACE(sizeof(int) * GSR_KMS_MAX_PLANES)];
+ memset(cmsgbuf, 0, sizeof(cmsgbuf));
+ response_message.msg_control = cmsgbuf;
+ response_message.msg_controllen = sizeof(cmsgbuf);
+
+ int res = recvmsg(server_fd, &response_message, MSG_WAITALL);
+ if(res <= 0)
+ return res;
+
+ if(response->num_fds > 0) {
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&response_message);
+ if(cmsg) {
+ int *fds = (int*)CMSG_DATA(cmsg);
+ for(int i = 0; i < response->num_fds; ++i) {
+ response->fds[i].fd = fds[i];
+ }
+ } else {
+ close_fds(response);
+ }
+ }
+
+ return res;
+}
+
+/* We have to use $HOME because in flatpak there is no simple path that is accessible, read and write, that multiple flatpak instances can access */
+static bool create_socket_path(char *output_path, size_t output_path_size) {
+ const char *home = getenv("HOME");
+ if(!home)
+ home = "/tmp";
+
+ char random_characters[11];
+ random_characters[10] = '\0';
+ if(!generate_random_characters(random_characters, 10, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 62))
+ return false;
+
+ snprintf(output_path, output_path_size, "%s/.gsr-kms-socket-%s", home, random_characters);
+ return true;
+}
+
+static void strncpy_safe(char *dst, const char *src, int len) {
+ int src_len = strlen(src);
+ int min_len = src_len;
+ if(len - 1 < min_len)
+ min_len = len - 1;
+ memcpy(dst, src, min_len);
+ dst[min_len] = '\0';
+}
+
+static bool find_program_in_path(const char *program_name, char *filepath, int filepath_len) {
+ const char *path = getenv("PATH");
+ if(!path)
+ return false;
+
+ int program_name_len = strlen(program_name);
+ const char *end = path + strlen(path);
+ while(path != end) {
+ const char *part_end = strchr(path, ':');
+ const char *next = part_end;
+ if(part_end) {
+ next = part_end + 1;
+ } else {
+ part_end = end;
+ next = end;
+ }
+
+ int len = part_end - path;
+ if(len + 1 + program_name_len < filepath_len) {
+ memcpy(filepath, path, len);
+ filepath[len] = '/';
+ memcpy(filepath + len + 1, program_name, program_name_len);
+ filepath[len + 1 + program_name_len] = '\0';
+
+ if(access(filepath, F_OK) == 0)
+ return true;
+ }
+
+ path = next;
+ }
+
+ return false;
+}
+
+int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) {
+ int result = -1;
+ self->kms_server_pid = -1;
+ self->initial_socket_fd = -1;
+ self->initial_client_fd = -1;
+ self->initial_socket_path[0] = '\0';
+ self->socket_pair[0] = -1;
+ self->socket_pair[1] = -1;
+ struct sockaddr_un local_addr = {0};
+ struct sockaddr_un remote_addr = {0};
+
+ if(!create_socket_path(self->initial_socket_path, sizeof(self->initial_socket_path))) {
+ fprintf(stderr, "gsr error: gsr_kms_client_init: failed to create path to kms socket\n");
+ return -1;
+ }
+
+ char server_filepath[PATH_MAX];
+ if(!find_program_in_path("gsr-kms-server", server_filepath, sizeof(server_filepath))) {
+ fprintf(stderr, "gsr error: gsr_kms_client_init: gsr-kms-server is not installed\n");
+ return -1;
+ }
+
+ const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
+ const char *home = getenv("HOME");
+ if(!home)
+ home = "/tmp";
+
+ bool has_perm = 0;
+ if(geteuid() == 0) {
+ has_perm = true;
+ } else {
+ cap_t kms_server_cap = cap_get_file(server_filepath);
+ if(kms_server_cap) {
+ cap_flag_value_t res = CAP_CLEAR;
+ cap_get_flag(kms_server_cap, CAP_SYS_ADMIN, CAP_PERMITTED, &res);
+ if(res == CAP_SET) {
+ //fprintf(stderr, "has permission!\n");
+ has_perm = true;
+ } else {
+ //fprintf(stderr, "No permission:(\n");
+ }
+ cap_free(kms_server_cap);
+ } else if(!inside_flatpak) {
+ if(errno == ENODATA)
+ fprintf(stderr, "gsr info: gsr_kms_client_init: gsr-kms-server is missing sys_admin cap and will require root authentication. To bypass this automatically, run: sudo setcap cap_sys_admin+ep '%s'\n", server_filepath);
+ else
+ fprintf(stderr, "gsr info: gsr_kms_client_init: failed to get cap\n");
+ }
+ }
+
+ if(socketpair(AF_UNIX, SOCK_STREAM, 0, self->socket_pair) == -1) {
+ fprintf(stderr, "gsr error: gsr_kms_client_init: socketpair failed, error: %s\n", strerror(errno));
+ goto err;
+ }
+
+ self->initial_socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if(self->initial_socket_fd == -1) {
+ fprintf(stderr, "gsr error: gsr_kms_client_init: socket failed, error: %s\n", strerror(errno));
+ goto err;
+ }
+
+ local_addr.sun_family = AF_UNIX;
+ strncpy_safe(local_addr.sun_path, self->initial_socket_path, sizeof(local_addr.sun_path));
+
+ const mode_t prev_mask = umask(0000);
+ const int bind_res = bind(self->initial_socket_fd, (struct sockaddr*)&local_addr, sizeof(local_addr.sun_family) + strlen(local_addr.sun_path));
+ umask(prev_mask);
+
+ if(bind_res == -1) {
+ fprintf(stderr, "gsr error: gsr_kms_client_init: failed to bind socket, error: %s\n", strerror(errno));
+ goto err;
+ }
+
+ if(listen(self->initial_socket_fd, 1) == -1) {
+ fprintf(stderr, "gsr error: gsr_kms_client_init: failed to listen on socket, error: %s\n", strerror(errno));
+ goto err;
+ }
+
+ pid_t pid = fork();
+ if(pid == -1) {
+ fprintf(stderr, "gsr error: gsr_kms_client_init: fork failed, error: %s\n", strerror(errno));
+ goto err;
+ } else if(pid == 0) { /* child */
+ if(inside_flatpak) {
+ const char *args[] = { "flatpak-spawn", "--host", "/var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/bin/kms-server-proxy", self->initial_socket_path, card_path, home, NULL };
+ execvp(args[0], (char *const*)args);
+ } else if(has_perm) {
+ const char *args[] = { server_filepath, self->initial_socket_path, card_path, NULL };
+ execvp(args[0], (char *const*)args);
+ } else {
+ const char *args[] = { "pkexec", server_filepath, self->initial_socket_path, card_path, NULL };
+ execvp(args[0], (char *const*)args);
+ }
+ fprintf(stderr, "gsr error: gsr_kms_client_init: execvp failed, error: %s\n", strerror(errno));
+ _exit(127);
+ } else { /* parent */
+ self->kms_server_pid = pid;
+ }
+
+ fprintf(stderr, "gsr info: gsr_kms_client_init: waiting for server to connect\n");
+ for(;;) {
+ struct timeval tv;
+ fd_set rfds;
+ FD_ZERO(&rfds);
+ FD_SET(self->initial_socket_fd, &rfds);
+
+ tv.tv_sec = 0;
+ tv.tv_usec = 100 * 1000; // 100 ms
+
+ int select_res = select(1 + self->initial_socket_fd, &rfds, NULL, NULL, &tv);
+ if(select_res > 0) {
+ socklen_t sock_len = 0;
+ self->initial_client_fd = accept(self->initial_socket_fd, (struct sockaddr*)&remote_addr, &sock_len);
+ if(self->initial_client_fd == -1) {
+ fprintf(stderr, "gsr error: gsr_kms_client_init: accept failed on socket, error: %s\n", strerror(errno));
+ goto err;
+ }
+ break;
+ } else {
+ int status = 0;
+ int wait_result = waitpid(self->kms_server_pid, &status, WNOHANG);
+ if(wait_result != 0) {
+ int exit_code = -1;
+ if(WIFEXITED(status))
+ exit_code = WEXITSTATUS(status);
+ fprintf(stderr, "gsr error: gsr_kms_client_init: kms server died or never started, exit code: %d\n", exit_code);
+ self->kms_server_pid = -1;
+ if(exit_code != 0)
+ result = exit_code;
+ goto err;
+ }
+ }
+ }
+ fprintf(stderr, "gsr info: gsr_kms_client_init: server connected\n");
+
+ fprintf(stderr, "gsr info: replacing file-backed unix domain socket with socketpair\n");
+ if(gsr_kms_client_replace_connection(self) != 0)
+ goto err;
+
+ cleanup_socket(self, false);
+ fprintf(stderr, "gsr info: using socketpair\n");
+
+ return 0;
+
+ err:
+ gsr_kms_client_deinit(self);
+ return result;
+}
+
+void cleanup_socket(gsr_kms_client *self, bool kill_server) {
+ if(self->initial_client_fd != -1) {
+ close(self->initial_client_fd);
+ self->initial_client_fd = -1;
+ }
+
+ if(self->initial_socket_fd != -1) {
+ close(self->initial_socket_fd);
+ self->initial_socket_fd = -1;
+ }
+
+ if(kill_server) {
+ for(int i = 0; i < 2; ++i) {
+ if(self->socket_pair[i] > 0) {
+ close(self->socket_pair[i]);
+ self->socket_pair[i] = -1;
+ }
+ }
+ }
+
+ if(kill_server && self->kms_server_pid != -1) {
+ kill(self->kms_server_pid, SIGKILL);
+ //int status;
+ //waitpid(self->kms_server_pid, &status, 0);
+ self->kms_server_pid = -1;
+ }
+
+ if(self->initial_socket_path[0] != '\0') {
+ remove(self->initial_socket_path);
+ self->initial_socket_path[0] = '\0';
+ }
+}
+
+void gsr_kms_client_deinit(gsr_kms_client *self) {
+ cleanup_socket(self, true);
+}
+
+int gsr_kms_client_replace_connection(gsr_kms_client *self) {
+ gsr_kms_response response;
+ response.version = 0;
+ response.result = KMS_RESULT_FAILED_TO_SEND;
+ response.err_msg[0] = '\0';
+
+ gsr_kms_request request;
+ request.version = GSR_KMS_PROTOCOL_VERSION;
+ request.type = KMS_REQUEST_TYPE_REPLACE_CONNECTION;
+ request.new_connection_fd = self->socket_pair[GSR_SOCKET_PAIR_REMOTE];
+ if(send_msg_to_server(self->initial_client_fd, &request) == -1) {
+ fprintf(stderr, "gsr error: gsr_kms_client_replace_connection: failed to send request message to server\n");
+ return -1;
+ }
+
+ const int recv_res = recv_msg_from_server(self->socket_pair[GSR_SOCKET_PAIR_LOCAL], &response);
+ if(recv_res == 0) {
+ fprintf(stderr, "gsr warning: gsr_kms_client_replace_connection: kms server shut down\n");
+ return -1;
+ } else if(recv_res == -1) {
+ fprintf(stderr, "gsr error: gsr_kms_client_replace_connection: failed to receive response\n");
+ return -1;
+ }
+
+ if(response.version != GSR_KMS_PROTOCOL_VERSION) {
+ fprintf(stderr, "gsr error: gsr_kms_client_replace_connection: expected gsr-kms-server protocol version to be %u, but it's %u\n", GSR_KMS_PROTOCOL_VERSION, response.version);
+ /*close_fds(response);*/
+ return -1;
+ }
+
+ return 0;
+}
+
+int gsr_kms_client_get_kms(gsr_kms_client *self, gsr_kms_response *response) {
+ response->version = 0;
+ response->result = KMS_RESULT_FAILED_TO_SEND;
+ response->err_msg[0] = '\0';
+
+ gsr_kms_request request;
+ request.version = GSR_KMS_PROTOCOL_VERSION;
+ request.type = KMS_REQUEST_TYPE_GET_KMS;
+ request.new_connection_fd = 0;
+ if(send_msg_to_server(self->socket_pair[GSR_SOCKET_PAIR_LOCAL], &request) == -1) {
+ fprintf(stderr, "gsr error: gsr_kms_client_get_kms: failed to send request message to server\n");
+ strcpy(response->err_msg, "failed to send");
+ return -1;
+ }
+
+ const int recv_res = recv_msg_from_server(self->socket_pair[GSR_SOCKET_PAIR_LOCAL], response);
+ if(recv_res == 0) {
+ fprintf(stderr, "gsr warning: gsr_kms_client_get_kms: kms server shut down\n");
+ strcpy(response->err_msg, "failed to receive");
+ return -1;
+ } else if(recv_res == -1) {
+ fprintf(stderr, "gsr error: gsr_kms_client_get_kms: failed to receive response\n");
+ strcpy(response->err_msg, "failed to receive");
+ return -1;
+ }
+
+ if(response->version != GSR_KMS_PROTOCOL_VERSION) {
+ fprintf(stderr, "gsr error: gsr_kms_client_get_kms: expected gsr-kms-server protocol version to be %u, but it's %u\n", GSR_KMS_PROTOCOL_VERSION, response->version);
+ /*close_fds(response);*/
+ strcpy(response->err_msg, "mismatching protocol version");
+ return -1;
+ }
+
+ return 0;
+}
diff --git a/kms/client/kms_client.h b/kms/client/kms_client.h
new file mode 100644
index 0000000..2d18848
--- /dev/null
+++ b/kms/client/kms_client.h
@@ -0,0 +1,24 @@
+#ifndef GSR_KMS_CLIENT_H
+#define GSR_KMS_CLIENT_H
+
+#include "../kms_shared.h"
+#include <sys/types.h>
+#include <limits.h>
+
+typedef struct gsr_kms_client gsr_kms_client;
+
+struct gsr_kms_client {
+ pid_t kms_server_pid;
+ int initial_socket_fd;
+ int initial_client_fd;
+ char initial_socket_path[PATH_MAX];
+ int socket_pair[2];
+};
+
+/* |card_path| should be a path to card, for example /dev/dri/card0 */
+int gsr_kms_client_init(gsr_kms_client *self, const char *card_path);
+void gsr_kms_client_deinit(gsr_kms_client *self);
+
+int gsr_kms_client_get_kms(gsr_kms_client *self, gsr_kms_response *response);
+
+#endif /* #define GSR_KMS_CLIENT_H */
diff --git a/kms/kms_shared.h b/kms/kms_shared.h
new file mode 100644
index 0000000..4fa9c38
--- /dev/null
+++ b/kms/kms_shared.h
@@ -0,0 +1,60 @@
+#ifndef GSR_KMS_SHARED_H
+#define GSR_KMS_SHARED_H
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <drm_mode.h>
+
+#define GSR_KMS_PROTOCOL_VERSION 2
+#define GSR_KMS_MAX_PLANES 10
+
+typedef struct gsr_kms_response_fd gsr_kms_response_fd;
+typedef struct gsr_kms_response gsr_kms_response;
+
+typedef enum {
+ KMS_REQUEST_TYPE_REPLACE_CONNECTION,
+ KMS_REQUEST_TYPE_GET_KMS
+} gsr_kms_request_type;
+
+typedef enum {
+ KMS_RESULT_OK,
+ KMS_RESULT_INVALID_REQUEST,
+ KMS_RESULT_FAILED_TO_GET_PLANE,
+ KMS_RESULT_FAILED_TO_GET_PLANES,
+ KMS_RESULT_FAILED_TO_SEND
+} gsr_kms_result;
+
+typedef struct {
+ uint32_t version; /* GSR_KMS_PROTOCOL_VERSION */
+ int type; /* gsr_kms_request_type */
+ int new_connection_fd;
+} gsr_kms_request;
+
+struct gsr_kms_response_fd {
+ int fd;
+ uint32_t width;
+ uint32_t height;
+ uint32_t pitch;
+ uint32_t offset;
+ uint32_t pixel_format;
+ uint64_t modifier;
+ uint32_t connector_id; /* 0 if unknown */
+ bool is_combined_plane;
+ bool is_cursor;
+ bool has_hdr_metadata;
+ int x;
+ int y;
+ int src_w;
+ int src_h;
+ struct hdr_output_metadata hdr_metadata;
+};
+
+struct gsr_kms_response {
+ uint32_t version; /* GSR_KMS_PROTOCOL_VERSION */
+ int result; /* gsr_kms_result */
+ char err_msg[128];
+ gsr_kms_response_fd fds[GSR_KMS_MAX_PLANES];
+ int num_fds;
+};
+
+#endif /* #define GSR_KMS_SHARED_H */
diff --git a/kms/server/.gitignore b/kms/server/.gitignore
new file mode 100644
index 0000000..97420ef
--- /dev/null
+++ b/kms/server/.gitignore
@@ -0,0 +1 @@
+sibs-build/
diff --git a/kms/server/kms_server.c b/kms/server/kms_server.c
new file mode 100644
index 0000000..09deaab
--- /dev/null
+++ b/kms/server/kms_server.c
@@ -0,0 +1,572 @@
+#include "../kms_shared.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <stdlib.h>
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <time.h>
+
+#include <xf86drm.h>
+#include <xf86drmMode.h>
+#include <drm_mode.h>
+
+#define MAX_CONNECTORS 32
+
+typedef struct {
+ int drmfd;
+ drmModePlaneResPtr planes;
+} gsr_drm;
+
+typedef struct {
+ uint32_t connector_id;
+ uint64_t crtc_id;
+ uint64_t hdr_metadata_blob_id;
+} connector_crtc_pair;
+
+typedef struct {
+ connector_crtc_pair maps[MAX_CONNECTORS];
+ int num_maps;
+} connector_to_crtc_map;
+
+static int max_int(int a, int b) {
+ return a > b ? a : b;
+}
+
+static int send_msg_to_client(int client_fd, gsr_kms_response *response) {
+ struct iovec iov;
+ iov.iov_base = response;
+ iov.iov_len = sizeof(*response);
+
+ struct msghdr response_message = {0};
+ response_message.msg_iov = &iov;
+ response_message.msg_iovlen = 1;
+
+ char cmsgbuf[CMSG_SPACE(sizeof(int) * max_int(1, response->num_fds))];
+ memset(cmsgbuf, 0, sizeof(cmsgbuf));
+
+ if(response->num_fds > 0) {
+ response_message.msg_control = cmsgbuf;
+ response_message.msg_controllen = sizeof(cmsgbuf);
+
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&response_message);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(int) * response->num_fds);
+
+ int *fds = (int*)CMSG_DATA(cmsg);
+ for(int i = 0; i < response->num_fds; ++i) {
+ fds[i] = response->fds[i].fd;
+ }
+
+ response_message.msg_controllen = cmsg->cmsg_len;
+ }
+
+ return sendmsg(client_fd, &response_message, 0);
+}
+
+static int recv_msg_from_client(int client_fd, gsr_kms_request *request) {
+ struct iovec iov;
+ iov.iov_base = request;
+ iov.iov_len = sizeof(*request);
+
+ struct msghdr response_message = {0};
+ response_message.msg_iov = &iov;
+ response_message.msg_iovlen = 1;
+
+ char cmsgbuf[CMSG_SPACE(sizeof(int) * 1)];
+ memset(cmsgbuf, 0, sizeof(cmsgbuf));
+ response_message.msg_control = cmsgbuf;
+ response_message.msg_controllen = sizeof(cmsgbuf);
+
+ int res = recvmsg(client_fd, &response_message, MSG_WAITALL);
+ if(res <= 0)
+ return res;
+
+ if(request->new_connection_fd > 0) {
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&response_message);
+ if(cmsg) {
+ int *fds = (int*)CMSG_DATA(cmsg);
+ request->new_connection_fd = fds[0];
+ } else {
+ if(request->new_connection_fd > 0) {
+ close(request->new_connection_fd);
+ request->new_connection_fd = 0;
+ }
+ }
+ }
+
+ return res;
+}
+
+static bool connector_get_property_by_name(int drmfd, drmModeConnectorPtr props, const char *name, uint64_t *result) {
+ for(int i = 0; i < props->count_props; ++i) {
+ drmModePropertyPtr prop = drmModeGetProperty(drmfd, props->props[i]);
+ if(prop) {
+ if(strcmp(name, prop->name) == 0) {
+ *result = props->prop_values[i];
+ drmModeFreeProperty(prop);
+ return true;
+ }
+ drmModeFreeProperty(prop);
+ }
+ }
+ return false;
+}
+
+typedef enum {
+ PLANE_PROPERTY_X = 1 << 0,
+ PLANE_PROPERTY_Y = 1 << 1,
+ PLANE_PROPERTY_SRC_X = 1 << 2,
+ PLANE_PROPERTY_SRC_Y = 1 << 3,
+ PLANE_PROPERTY_SRC_W = 1 << 4,
+ PLANE_PROPERTY_SRC_H = 1 << 5,
+ PLANE_PROPERTY_IS_CURSOR = 1 << 6,
+ PLANE_PROPERTY_IS_PRIMARY = 1 << 7,
+} plane_property_mask;
+
+/* Returns plane_property_mask */
+static uint32_t plane_get_properties(int drmfd, uint32_t plane_id, int *x, int *y, int *src_x, int *src_y, int *src_w, int *src_h) {
+ *x = 0;
+ *y = 0;
+ *src_x = 0;
+ *src_y = 0;
+ *src_w = 0;
+ *src_h = 0;
+
+ plane_property_mask property_mask = 0;
+
+ drmModeObjectPropertiesPtr props = drmModeObjectGetProperties(drmfd, plane_id, DRM_MODE_OBJECT_PLANE);
+ if(!props)
+ return property_mask;
+
+ // TODO: Dont do this every frame
+ for(uint32_t i = 0; i < props->count_props; ++i) {
+ drmModePropertyPtr prop = drmModeGetProperty(drmfd, props->props[i]);
+ if(!prop)
+ continue;
+
+ // SRC_* values are fixed 16.16 points
+ const uint32_t type = prop->flags & (DRM_MODE_PROP_LEGACY_TYPE | DRM_MODE_PROP_EXTENDED_TYPE);
+ if((type & DRM_MODE_PROP_SIGNED_RANGE) && strcmp(prop->name, "CRTC_X") == 0) {
+ *x = (int)props->prop_values[i];
+ property_mask |= PLANE_PROPERTY_X;
+ } else if((type & DRM_MODE_PROP_SIGNED_RANGE) && strcmp(prop->name, "CRTC_Y") == 0) {
+ *y = (int)props->prop_values[i];
+ property_mask |= PLANE_PROPERTY_Y;
+ } else if((type & DRM_MODE_PROP_RANGE) && strcmp(prop->name, "SRC_X") == 0) {
+ *src_x = (int)(props->prop_values[i] >> 16);
+ property_mask |= PLANE_PROPERTY_SRC_X;
+ } else if((type & DRM_MODE_PROP_RANGE) && strcmp(prop->name, "SRC_Y") == 0) {
+ *src_y = (int)(props->prop_values[i] >> 16);
+ property_mask |= PLANE_PROPERTY_SRC_Y;
+ } else if((type & DRM_MODE_PROP_RANGE) && strcmp(prop->name, "SRC_W") == 0) {
+ *src_w = (int)(props->prop_values[i] >> 16);
+ property_mask |= PLANE_PROPERTY_SRC_W;
+ } else if((type & DRM_MODE_PROP_RANGE) && strcmp(prop->name, "SRC_H") == 0) {
+ *src_h = (int)(props->prop_values[i] >> 16);
+ property_mask |= PLANE_PROPERTY_SRC_H;
+ } else if((type & DRM_MODE_PROP_ENUM) && strcmp(prop->name, "type") == 0) {
+ const uint64_t current_enum_value = props->prop_values[i];
+ for(int j = 0; j < prop->count_enums; ++j) {
+ if(prop->enums[j].value == current_enum_value && strcmp(prop->enums[j].name, "Primary") == 0) {
+ property_mask |= PLANE_PROPERTY_IS_PRIMARY;
+ break;
+ } else if(prop->enums[j].value == current_enum_value && strcmp(prop->enums[j].name, "Cursor") == 0) {
+ property_mask |= PLANE_PROPERTY_IS_CURSOR;
+ break;
+ }
+ }
+ }
+
+ drmModeFreeProperty(prop);
+ }
+
+ drmModeFreeObjectProperties(props);
+ return property_mask;
+}
+
+/* Returns 0 if not found */
+static const connector_crtc_pair* get_connector_pair_by_crtc_id(const connector_to_crtc_map *c2crtc_map, uint32_t crtc_id) {
+ for(int i = 0; i < c2crtc_map->num_maps; ++i) {
+ if(c2crtc_map->maps[i].crtc_id == crtc_id)
+ return &c2crtc_map->maps[i];
+ }
+ return NULL;
+}
+
+static void map_crtc_to_connector_ids(gsr_drm *drm, connector_to_crtc_map *c2crtc_map) {
+ c2crtc_map->num_maps = 0;
+ drmModeResPtr resources = drmModeGetResources(drm->drmfd);
+ if(!resources)
+ return;
+
+ for(int i = 0; i < resources->count_connectors && c2crtc_map->num_maps < MAX_CONNECTORS; ++i) {
+ drmModeConnectorPtr connector = drmModeGetConnectorCurrent(drm->drmfd, resources->connectors[i]);
+ if(!connector)
+ continue;
+
+ uint64_t crtc_id = 0;
+ connector_get_property_by_name(drm->drmfd, connector, "CRTC_ID", &crtc_id);
+
+ uint64_t hdr_output_metadata_blob_id = 0;
+ connector_get_property_by_name(drm->drmfd, connector, "HDR_OUTPUT_METADATA", &hdr_output_metadata_blob_id);
+
+ c2crtc_map->maps[c2crtc_map->num_maps].connector_id = connector->connector_id;
+ c2crtc_map->maps[c2crtc_map->num_maps].crtc_id = crtc_id;
+ c2crtc_map->maps[c2crtc_map->num_maps].hdr_metadata_blob_id = hdr_output_metadata_blob_id;
+ ++c2crtc_map->num_maps;
+
+ drmModeFreeConnector(connector);
+ }
+ drmModeFreeResources(resources);
+}
+
+static void drm_mode_cleanup_handles(int drmfd, drmModeFB2Ptr drmfb) {
+ for(int i = 0; i < 4; ++i) {
+ if(!drmfb->handles[i])
+ continue;
+
+ bool already_closed = false;
+ for(int j = 0; j < i; ++j) {
+ if(drmfb->handles[i] == drmfb->handles[j]) {
+ already_closed = true;
+ break;
+ }
+ }
+
+ if(already_closed)
+ continue;
+
+ drmCloseBufferHandle(drmfd, drmfb->handles[i]);
+ }
+}
+
+static bool get_hdr_metadata(int drm_fd, uint64_t hdr_metadata_blob_id, struct hdr_output_metadata *hdr_metadata) {
+ drmModePropertyBlobPtr hdr_metadata_blob = drmModeGetPropertyBlob(drm_fd, hdr_metadata_blob_id);
+ if(!hdr_metadata_blob)
+ return false;
+
+ if(hdr_metadata_blob->length >= sizeof(struct hdr_output_metadata))
+ *hdr_metadata = *(struct hdr_output_metadata*)hdr_metadata_blob->data;
+
+ drmModeFreePropertyBlob(hdr_metadata_blob);
+ return true;
+}
+
+static int kms_get_fb(gsr_drm *drm, gsr_kms_response *response, connector_to_crtc_map *c2crtc_map) {
+ int result = -1;
+
+ response->result = KMS_RESULT_OK;
+ response->err_msg[0] = '\0';
+ response->num_fds = 0;
+
+ for(uint32_t i = 0; i < drm->planes->count_planes && response->num_fds < GSR_KMS_MAX_PLANES; ++i) {
+ drmModePlanePtr plane = NULL;
+ drmModeFB2Ptr drmfb = NULL;
+
+ plane = drmModeGetPlane(drm->drmfd, drm->planes->planes[i]);
+ if(!plane) {
+ response->result = KMS_RESULT_FAILED_TO_GET_PLANE;
+ snprintf(response->err_msg, sizeof(response->err_msg), "failed to get drm plane with id %u, error: %s\n", drm->planes->planes[i], strerror(errno));
+ fprintf(stderr, "kms server error: %s\n", response->err_msg);
+ goto next;
+ }
+
+ if(!plane->fb_id)
+ goto next;
+
+ drmfb = drmModeGetFB2(drm->drmfd, plane->fb_id);
+ if(!drmfb) {
+ // Commented out for now because we get here if the cursor is moved to another monitor and we dont care about the cursor
+ //response->result = KMS_RESULT_FAILED_TO_GET_PLANE;
+ //snprintf(response->err_msg, sizeof(response->err_msg), "drmModeGetFB2 failed, error: %s", strerror(errno));
+ //fprintf(stderr, "kms server error: %s\n", response->err_msg);
+ goto next;
+ }
+
+ if(!drmfb->handles[0]) {
+ response->result = KMS_RESULT_FAILED_TO_GET_PLANE;
+ snprintf(response->err_msg, sizeof(response->err_msg), "drmfb handle is NULL");
+ fprintf(stderr, "kms server error: %s\n", response->err_msg);
+ goto cleanup_handles;
+ }
+
+ // TODO: Check if dimensions have changed by comparing width and height to previous time this was called.
+ // TODO: Support other plane formats than rgb (with multiple planes, such as direct YUV420 on wayland).
+
+ int fb_fd = -1;
+ const int ret = drmPrimeHandleToFD(drm->drmfd, drmfb->handles[0], O_RDONLY, &fb_fd);
+ if(ret != 0 || fb_fd == -1) {
+ response->result = KMS_RESULT_FAILED_TO_GET_PLANE;
+ snprintf(response->err_msg, sizeof(response->err_msg), "failed to get fd from drm handle, error: %s", strerror(errno));
+ fprintf(stderr, "kms server error: %s\n", response->err_msg);
+ goto cleanup_handles;
+ }
+
+ const int fd_index = response->num_fds;
+
+ int x = 0, y = 0, src_x = 0, src_y = 0, src_w = 0, src_h = 0;
+ plane_property_mask property_mask = plane_get_properties(drm->drmfd, plane->plane_id, &x, &y, &src_x, &src_y, &src_w, &src_h);
+ if((property_mask & PLANE_PROPERTY_IS_PRIMARY) || (property_mask & PLANE_PROPERTY_IS_CURSOR)) {
+ const connector_crtc_pair *crtc_pair = get_connector_pair_by_crtc_id(c2crtc_map, plane->crtc_id);
+ if(crtc_pair && crtc_pair->hdr_metadata_blob_id) {
+ response->fds[fd_index].has_hdr_metadata = get_hdr_metadata(drm->drmfd, crtc_pair->hdr_metadata_blob_id, &response->fds[fd_index].hdr_metadata);
+ } else {
+ response->fds[fd_index].has_hdr_metadata = false;
+ }
+
+ response->fds[fd_index].fd = fb_fd;
+ response->fds[fd_index].width = drmfb->width;
+ response->fds[fd_index].height = drmfb->height;
+ response->fds[fd_index].pitch = drmfb->pitches[0];
+ response->fds[fd_index].offset = drmfb->offsets[0];
+ response->fds[fd_index].pixel_format = drmfb->pixel_format;
+ response->fds[fd_index].modifier = drmfb->modifier;
+ response->fds[fd_index].connector_id = crtc_pair ? crtc_pair->connector_id : 0;
+ response->fds[fd_index].is_cursor = property_mask & PLANE_PROPERTY_IS_CURSOR;
+ response->fds[fd_index].is_combined_plane = false;
+ if(property_mask & PLANE_PROPERTY_IS_CURSOR) {
+ response->fds[fd_index].x = x;
+ response->fds[fd_index].y = y;
+ response->fds[fd_index].src_w = 0;
+ response->fds[fd_index].src_h = 0;
+ } else {
+ response->fds[fd_index].x = src_x;
+ response->fds[fd_index].y = src_y;
+ response->fds[fd_index].src_w = src_w;
+ response->fds[fd_index].src_h = src_h;
+ }
+ ++response->num_fds;
+ } else {
+ close(fb_fd);
+ }
+
+ cleanup_handles:
+ drm_mode_cleanup_handles(drm->drmfd, drmfb);
+
+ next:
+ if(drmfb)
+ drmModeFreeFB2(drmfb);
+ if(plane)
+ drmModeFreePlane(plane);
+ }
+
+ if(response->num_fds > 0)
+ response->result = KMS_RESULT_OK;
+
+ if(response->result == KMS_RESULT_OK) {
+ result = 0;
+ } else {
+ for(int i = 0; i < response->num_fds; ++i) {
+ close(response->fds[i].fd);
+ }
+ response->num_fds = 0;
+ }
+
+ return result;
+}
+
+static double clock_get_monotonic_seconds(void) {
+ struct timespec ts;
+ ts.tv_sec = 0;
+ ts.tv_nsec = 0;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (double)ts.tv_sec + (double)ts.tv_nsec * 0.000000001;
+}
+
+static void strncpy_safe(char *dst, const char *src, int len) {
+ int src_len = strlen(src);
+ int min_len = src_len;
+ if(len - 1 < min_len)
+ min_len = len - 1;
+ memcpy(dst, src, min_len);
+ dst[min_len] = '\0';
+}
+
+int main(int argc, char **argv) {
+ int res = 0;
+ int socket_fd = 0;
+ gsr_drm drm;
+ drm.drmfd = 0;
+ drm.planes = NULL;
+
+ if(argc != 3) {
+ fprintf(stderr, "usage: gsr-kms-server <domain_socket_path> <card_path>\n");
+ return 1;
+ }
+
+ const char *domain_socket_path = argv[1];
+ socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if(socket_fd == -1) {
+ fprintf(stderr, "kms server error: failed to create socket, error: %s\n", strerror(errno));
+ return 2;
+ }
+
+ const char *card_path = argv[2];
+
+ drm.drmfd = open(card_path, O_RDONLY);
+ if(drm.drmfd < 0) {
+ fprintf(stderr, "kms server error: failed to open %s, error: %s", card_path, strerror(errno));
+ res = 2;
+ goto done;
+ }
+
+ if(drmSetClientCap(drm.drmfd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1) != 0) {
+ fprintf(stderr, "kms server error: drmSetClientCap DRM_CLIENT_CAP_UNIVERSAL_PLANES failed, error: %s\n", strerror(errno));
+ res = 2;
+ goto done;
+ }
+
+ if(drmSetClientCap(drm.drmfd, DRM_CLIENT_CAP_ATOMIC, 1) != 0) {
+ fprintf(stderr, "kms server warning: drmSetClientCap DRM_CLIENT_CAP_ATOMIC failed, error: %s. The wrong monitor may be captured as a result\n", strerror(errno));
+ }
+
+ drm.planes = drmModeGetPlaneResources(drm.drmfd);
+ if(!drm.planes) {
+ fprintf(stderr, "kms server error: failed to get plane resources, error: %s\n", strerror(errno));
+ res = 2;
+ goto done;
+ }
+
+ connector_to_crtc_map c2crtc_map;
+ c2crtc_map.num_maps = 0;
+ map_crtc_to_connector_ids(&drm, &c2crtc_map);
+
+ fprintf(stderr, "kms server info: connecting to the client\n");
+ bool connected = false;
+ const double connect_timeout_sec = 5.0;
+ const double start_time = clock_get_monotonic_seconds();
+ while(clock_get_monotonic_seconds() - start_time < connect_timeout_sec) {
+ struct sockaddr_un remote_addr = {0};
+ remote_addr.sun_family = AF_UNIX;
+ strncpy_safe(remote_addr.sun_path, domain_socket_path, sizeof(remote_addr.sun_path));
+ // TODO: Check if parent disconnected
+ if(connect(socket_fd, (struct sockaddr*)&remote_addr, sizeof(remote_addr.sun_family) + strlen(remote_addr.sun_path)) == -1) {
+ if(errno == ECONNREFUSED || errno == ENOENT) {
+ goto next;
+ } else if(errno == EISCONN) {
+ connected = true;
+ break;
+ }
+
+ fprintf(stderr, "kms server error: connect failed, error: %s (%d)\n", strerror(errno), errno);
+ res = 2;
+ goto done;
+ }
+
+ next:
+ usleep(30 * 1000); // 30 milliseconds
+ }
+
+ if(connected) {
+ fprintf(stderr, "kms server info: connected to the client\n");
+ } else {
+ fprintf(stderr, "kms server error: failed to connect to the client in %f seconds\n", connect_timeout_sec);
+ res = 2;
+ goto done;
+ }
+
+ for(;;) {
+ gsr_kms_request request;
+ request.version = 0;
+ request.type = -1;
+ request.new_connection_fd = 0;
+
+ const int recv_res = recv_msg_from_client(socket_fd, &request);
+ if(recv_res == 0) {
+ fprintf(stderr, "kms server info: kms client shutdown, shutting down the server\n");
+ res = 3;
+ goto done;
+ } else if(recv_res == -1) {
+ const int err = errno;
+ fprintf(stderr, "kms server error: failed to read all data in client request (error: %s), ignoring\n", strerror(err));
+ if(err == EBADF) {
+ fprintf(stderr, "kms server error: invalid client fd, shutting down the server\n");
+ res = 3;
+ goto done;
+ }
+ continue;
+ }
+
+ if(request.version != GSR_KMS_PROTOCOL_VERSION) {
+ fprintf(stderr, "kms server error: expected gpu screen recorder protocol version to be %u, but it's %u\n", GSR_KMS_PROTOCOL_VERSION, request.version);
+ /*
+ if(request.new_connection_fd > 0)
+ close(request.new_connection_fd);
+ */
+ continue;
+ }
+
+ switch(request.type) {
+ case KMS_REQUEST_TYPE_REPLACE_CONNECTION: {
+ gsr_kms_response response;
+ response.version = GSR_KMS_PROTOCOL_VERSION;
+ response.num_fds = 0;
+
+ if(request.new_connection_fd > 0) {
+ if(socket_fd > 0)
+ close(socket_fd);
+ socket_fd = request.new_connection_fd;
+
+ response.result = KMS_RESULT_OK;
+ if(send_msg_to_client(socket_fd, &response) == -1)
+ fprintf(stderr, "kms server error: failed to respond to client KMS_REQUEST_TYPE_REPLACE_CONNECTION request\n");
+ } else {
+ response.result = KMS_RESULT_INVALID_REQUEST;
+ snprintf(response.err_msg, sizeof(response.err_msg), "received invalid connection fd");
+ fprintf(stderr, "kms server error: %s\n", response.err_msg);
+ if(send_msg_to_client(socket_fd, &response) == -1)
+ fprintf(stderr, "kms server error: failed to respond to client request\n");
+ }
+
+ break;
+ }
+ case KMS_REQUEST_TYPE_GET_KMS: {
+ gsr_kms_response response;
+ response.version = GSR_KMS_PROTOCOL_VERSION;
+ response.num_fds = 0;
+
+ if(kms_get_fb(&drm, &response, &c2crtc_map) == 0) {
+ if(send_msg_to_client(socket_fd, &response) == -1)
+ fprintf(stderr, "kms server error: failed to respond to client KMS_REQUEST_TYPE_GET_KMS request\n");
+ } else {
+ if(send_msg_to_client(socket_fd, &response) == -1)
+ fprintf(stderr, "kms server error: failed to respond to client KMS_REQUEST_TYPE_GET_KMS request\n");
+ }
+
+ for(int i = 0; i < response.num_fds; ++i) {
+ close(response.fds[i].fd);
+ }
+
+ break;
+ }
+ default: {
+ gsr_kms_response response;
+ response.version = GSR_KMS_PROTOCOL_VERSION;
+ response.result = KMS_RESULT_INVALID_REQUEST;
+ response.num_fds = 0;
+
+ snprintf(response.err_msg, sizeof(response.err_msg), "invalid request type %d, expected %d (%s)", request.type, KMS_REQUEST_TYPE_GET_KMS, "KMS_REQUEST_TYPE_GET_KMS");
+ fprintf(stderr, "kms server error: %s\n", response.err_msg);
+ if(send_msg_to_client(socket_fd, &response) == -1)
+ fprintf(stderr, "kms server error: failed to respond to client request\n");
+
+ break;
+ }
+ }
+ }
+
+ done:
+ if(drm.planes)
+ drmModeFreePlaneResources(drm.planes);
+ if(drm.drmfd > 0)
+ close(drm.drmfd);
+ if(socket_fd > 0)
+ close(socket_fd);
+ return res;
+}
diff --git a/kms/server/project.conf b/kms/server/project.conf
new file mode 100644
index 0000000..26a1947
--- /dev/null
+++ b/kms/server/project.conf
@@ -0,0 +1,11 @@
+[package]
+name = "gsr-kms-server"
+type = "executable"
+version = "1.0.0"
+platforms = ["posix"]
+
+[config]
+error_on_warning = "true"
+
+[dependencies]
+libdrm = ">=2"
diff --git a/project.conf b/project.conf
index 4532649..a046427 100644
--- a/project.conf
+++ b/project.conf
@@ -1,29 +1,26 @@
[package]
name = "gpu-screen-recorder"
type = "executable"
-version = "1.1.0"
+version = "3.0.0"
platforms = ["posix"]
[config]
-include_dirs = ["/opt/cuda/targets/x86_64-linux/include"]
-libs = ["/usr/lib64/libcuda.so"]
-
-[define]
-PULSEAUDIO = "1"
+ignore_dirs = ["kms/server"]
+error_on_warning = "true"
[dependencies]
-glew = ">=2"
libavcodec = ">=58"
libavformat = ">=58"
libavutil = ">=56.2"
x11 = ">=1"
xcomposite = ">=0.2"
-#xdamage = "1"
-
-# TODO: Remove this dependency, this is needed right now for glfwMakeContextCurrent
-glfw3 = "3"
-
-#alsa = "1"
-libpulse-simple = ">=13"
-
+xrandr = ">=1"
+xfixes = ">=2"
+libpulse = ">=13"
libswresample = ">=3"
+libavfilter = ">=5"
+libva = ">=1"
+libcap = ">=2"
+libdrm = ">=2"
+wayland-egl = ">=15"
+wayland-client = ">=1"
diff --git a/interactive.sh b/scripts/interactive.sh
index c02e7e9..63b0eae 100755
--- a/interactive.sh
+++ b/scripts/interactive.sh
@@ -14,4 +14,4 @@ read output_file_name
output_dir=$(dirname "$output_file_name")
mkdir -p "$output_dir"
-./gpu-screen-recorder -w "$window_id" -c mp4 -f "$fps" -a "$selected_audio_input" -o "$output_file_name"
+gpu-screen-recorder -w "$window_id" -c mp4 -f "$fps" -a "$selected_audio_input" -o "$output_file_name"
diff --git a/list-sinks.sh b/scripts/list-sinks.sh
index e5fc44f..e5fc44f 100755
--- a/list-sinks.sh
+++ b/scripts/list-sinks.sh
diff --git a/scripts/record-application-name.sh b/scripts/record-application-name.sh
new file mode 100755
index 0000000..0411781
--- /dev/null
+++ b/scripts/record-application-name.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+window=$(xdotool selectwindow)
+window_name=$(xdotool getwindowclassname "$window" || xdotool getwindowname "$window" || echo "game")
+window_name="$(echo "$window_name" | tr '/\\' '_')"
+gpu-screen-recorder -w "$window" -f 60 -a "$(pactl get-default-sink).monitor" -o "$HOME/Videos/recording/$window_name/$(date +"Video_%Y-%m-%d_%H-%M-%S.mp4")"
diff --git a/scripts/replay-application-name.sh b/scripts/replay-application-name.sh
new file mode 100755
index 0000000..826ca3b
--- /dev/null
+++ b/scripts/replay-application-name.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+window=$(xdotool selectwindow)
+window_name=$(xdotool getwindowclassname "$window" || xdotool getwindowname "$window" || echo "game")
+window_name="$(echo "$window_name" | tr '/\\' '_')"
+gpu-screen-recorder -w "$window" -f 60 -c mkv -a "$(pactl get-default-sink).monitor" -r 60 -o "$HOME/Videos/replay/$window_name"
diff --git a/replay.sh b/scripts/replay.sh
index 6bbbb54..2781e1e 100755
--- a/replay.sh
+++ b/scripts/replay.sh
@@ -3,4 +3,4 @@
[ "$#" -ne 4 ] && echo "usage: replay.sh <window_id> <fps> <replay_time_sec> <output_directory>" && exit 1
active_sink="$(pactl get-default-sink).monitor"
mkdir -p "$4"
-./gpu-screen-recorder -w "$1" -c mp4 -f "$2" -a "$active_sink" -r "$3" -o "$4"
+gpu-screen-recorder -w "$1" -c mkv -f "$2" -a "$active_sink" -r "$3" -o "$4"
diff --git a/scripts/save-replay.sh b/scripts/save-replay.sh
new file mode 100755
index 0000000..eac9141
--- /dev/null
+++ b/scripts/save-replay.sh
@@ -0,0 +1,4 @@
+#!/bin/sh -e
+
+killall -SIGUSR1 gpu-screen-recorder
+notify-send -t 5000 -u low -- "GPU Screen Recorder" "Replay saved"
diff --git a/scripts/start-replay.sh b/scripts/start-replay.sh
new file mode 100755
index 0000000..e36d59d
--- /dev/null
+++ b/scripts/start-replay.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+video_path="$HOME/Videos"
+mkdir -p "$video_path"
+gpu-screen-recorder -w screen -f 60 -a "$(pactl get-default-sink).monitor" -c mkv -r 30 -o "$video_path"
diff --git a/scripts/stop-replay.sh b/scripts/stop-replay.sh
new file mode 100755
index 0000000..d38da9c
--- /dev/null
+++ b/scripts/stop-replay.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+killall -SIGINT gpu-screen-recorder
diff --git a/toggle-recording-selected.sh b/scripts/toggle-recording-selected.sh
index 663f360..f87f71c 100755
--- a/toggle-recording-selected.sh
+++ b/scripts/toggle-recording-selected.sh
@@ -5,5 +5,5 @@ window=$(xdotool selectwindow)
active_sink="$(pactl get-default-sink).monitor"
mkdir -p "$HOME/Videos"
video="$HOME/Videos/$(date +"Video_%Y-%m-%d_%H-%M-%S.mp4")"
-notify-send -u low 'GPU Screen Recorder' "Started recording video to $video"
+notify-send -t 5000 -u low 'GPU Screen Recorder' "Started recording video to $video"
gpu-screen-recorder -w "$window" -c mp4 -f 60 -a "$active_sink" -o "$video"
diff --git a/twitch-stream-local-copy.sh b/scripts/twitch-stream-local-copy.sh
index 1128a5e..dba9d15 100755
--- a/twitch-stream-local-copy.sh
+++ b/scripts/twitch-stream-local-copy.sh
@@ -4,4 +4,4 @@
[ "$#" -ne 4 ] && echo "usage: twitch-stream-local-copy.sh <window_id> <fps> <livestream_key> <local_file>" && exit 1
active_sink="$(pactl get-default-sink).monitor"
-./gpu-screen-recorder -w "$1" -c flv -f "$2" -a "$active_sink" | tee -- "$4" | ffmpeg -i pipe:0 -c:v copy -f flv -- "rtmp://live.twitch.tv/app/$3"
+gpu-screen-recorder -w "$1" -c flv -f "$2" -a "$active_sink" | tee -- "$4" | ffmpeg -i pipe:0 -c copy -f flv -- "rtmp://live.twitch.tv/app/$3"
diff --git a/twitch-stream.sh b/scripts/twitch-stream.sh
index 28d0eb1..cd4737a 100755
--- a/twitch-stream.sh
+++ b/scripts/twitch-stream.sh
@@ -2,4 +2,4 @@
[ "$#" -ne 3 ] && echo "usage: twitch-stream.sh <window_id> <fps> <livestream_key>" && exit 1
active_sink="$(pactl get-default-sink).monitor"
-./gpu-screen-recorder -w "$1" -c flv -f "$2" -a "$active_sink" | ffmpeg -i pipe:0 -c:v copy -f flv -- "rtmp://live.twitch.tv/app/$3"
+gpu-screen-recorder -w "$1" -c flv -f "$2" -a "$active_sink" -o "rtmp://live.twitch.tv/app/$3"
diff --git a/scripts/youtube-hls-stream.sh b/scripts/youtube-hls-stream.sh
new file mode 100755
index 0000000..21619af
--- /dev/null
+++ b/scripts/youtube-hls-stream.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+[ "$#" -ne 3 ] && echo "usage: youtube-hls-stream.sh <window_id> <fps> <livestream_key>" && exit 1
+mkdir "youtube_stream"
+cd "youtube_stream"
+active_sink="$(pactl get-default-sink).monitor"
+gpu-screen-recorder -w "$1" -c mpegts -f "$2" -a "$active_sink" | ffmpeg -i pipe:0 -c copy -f hls \
+ -hls_time 2 -hls_flags independent_segments -hls_flags delete_segments -hls_segment_type mpegts -hls_segment_filename stream%02d.ts -master_pl_name stream.m3u8 out1 &
+echo "Waiting until stream segments are created..."
+sleep 10
+ffmpeg -i stream.m3u8 -c copy -- "https://a.upload.youtube.com/http_upload_hls?cid=$3&copy=0&file=stream.m3u8"
diff --git a/src/capture/capture.c b/src/capture/capture.c
new file mode 100644
index 0000000..cec0b0d
--- /dev/null
+++ b/src/capture/capture.c
@@ -0,0 +1,400 @@
+#include "../../include/capture/capture.h"
+#include "../../include/egl.h"
+#include "../../include/cuda.h"
+#include "../../include/utils.h"
+#include <stdio.h>
+#include <stdint.h>
+#include <va/va.h>
+#include <va/va_drmcommon.h>
+#include <libavutil/frame.h>
+#include <libavutil/hwcontext_vaapi.h>
+#include <libavutil/hwcontext_cuda.h>
+#include <libavcodec/avcodec.h>
+
+#define FOURCC_NV12 842094158
+#define FOURCC_P010 808530000
+
+int gsr_capture_start(gsr_capture *cap, AVCodecContext *video_codec_context, AVFrame *frame) {
+ if(cap->started)
+ return -1;
+
+ int res = cap->start(cap, video_codec_context, frame);
+ if(res == 0)
+ cap->started = true;
+
+ return res;
+}
+
+void gsr_capture_tick(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ if(!cap->started) {
+ fprintf(stderr, "gsr error: gsp_capture_tick failed: the gsr capture has not been started\n");
+ return;
+ }
+
+ if(cap->tick)
+ cap->tick(cap, video_codec_context);
+}
+
+bool gsr_capture_should_stop(gsr_capture *cap, bool *err) {
+ if(!cap->started) {
+ fprintf(stderr, "gsr error: gsr_capture_should_stop failed: the gsr capture has not been started\n");
+ return false;
+ }
+
+ if(!cap->should_stop)
+ return false;
+
+ return cap->should_stop(cap, err);
+}
+
+int gsr_capture_capture(gsr_capture *cap, AVFrame *frame) {
+ if(!cap->started) {
+ fprintf(stderr, "gsr error: gsr_capture_capture failed: the gsr capture has not been started\n");
+ return -1;
+ }
+ return cap->capture(cap, frame);
+}
+
+void gsr_capture_end(gsr_capture *cap, AVFrame *frame) {
+ if(!cap->started) {
+ fprintf(stderr, "gsr error: gsr_capture_end failed: the gsr capture has not been started\n");
+ return;
+ }
+
+ if(!cap->capture_end)
+ return;
+
+ cap->capture_end(cap, frame);
+}
+
+void gsr_capture_destroy(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ cap->destroy(cap, video_codec_context);
+}
+
+static uint32_t fourcc(uint32_t a, uint32_t b, uint32_t c, uint32_t d) {
+ return (d << 24) | (c << 16) | (b << 8) | a;
+}
+
+bool gsr_capture_base_setup_vaapi_textures(gsr_capture_base *self, AVFrame *frame, VADisplay va_dpy, VADRMPRIMESurfaceDescriptor *prime, gsr_color_range color_range) {
+ const int res = av_hwframe_get_buffer(self->video_codec_context->hw_frames_ctx, frame, 0);
+ if(res < 0) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_vaapi_textures: av_hwframe_get_buffer failed: %d\n", res);
+ return false;
+ }
+
+ VASurfaceID target_surface_id = (uintptr_t)frame->data[3];
+
+ VAStatus va_status = vaExportSurfaceHandle(va_dpy, target_surface_id, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, VA_EXPORT_SURFACE_WRITE_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS, prime);
+ if(va_status != VA_STATUS_SUCCESS) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_vaapi_textures: vaExportSurfaceHandle failed, error: %d\n", va_status);
+ return false;
+ }
+ vaSyncSurface(va_dpy, target_surface_id);
+
+ self->egl->glGenTextures(1, &self->input_texture);
+ self->egl->glBindTexture(GL_TEXTURE_2D, self->input_texture);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ self->egl->glGenTextures(1, &self->cursor_texture);
+ self->egl->glBindTexture(GL_TEXTURE_2D, self->cursor_texture);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ const uint32_t formats_nv12[2] = { fourcc('R', '8', ' ', ' '), fourcc('G', 'R', '8', '8') };
+ const uint32_t formats_p010[2] = { fourcc('R', '1', '6', ' '), fourcc('G', 'R', '3', '2') };
+
+ if(prime->fourcc == FOURCC_NV12 || prime->fourcc == FOURCC_P010) {
+ const uint32_t *formats = prime->fourcc == FOURCC_NV12 ? formats_nv12 : formats_p010;
+ const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size
+
+ self->egl->glGenTextures(2, self->target_textures);
+ for(int i = 0; i < 2; ++i) {
+ const int layer = i;
+ const int plane = 0;
+
+ //const uint64_t modifier = prime->objects[prime->layers[layer].object_index[plane]].drm_format_modifier;
+
+ const intptr_t img_attr[] = {
+ EGL_LINUX_DRM_FOURCC_EXT, formats[i],
+ EGL_WIDTH, prime->width / div[i],
+ EGL_HEIGHT, prime->height / div[i],
+ EGL_DMA_BUF_PLANE0_FD_EXT, prime->objects[prime->layers[layer].object_index[plane]].fd,
+ EGL_DMA_BUF_PLANE0_OFFSET_EXT, prime->layers[layer].offset[plane],
+ EGL_DMA_BUF_PLANE0_PITCH_EXT, prime->layers[layer].pitch[plane],
+ // TODO:
+ //EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, modifier & 0xFFFFFFFFULL,
+ //EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, modifier >> 32ULL,
+ EGL_NONE
+ };
+
+ while(self->egl->eglGetError() != EGL_SUCCESS){}
+ EGLImage image = self->egl->eglCreateImage(self->egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, img_attr);
+ if(!image) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_vaapi_textures: failed to create egl image from drm fd for output drm fd, error: %d\n", self->egl->eglGetError());
+ return false;
+ }
+
+ self->egl->glBindTexture(GL_TEXTURE_2D, self->target_textures[i]);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ while(self->egl->glGetError()) {}
+ while(self->egl->eglGetError() != EGL_SUCCESS){}
+ self->egl->glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image);
+ if(self->egl->glGetError() != 0 || self->egl->eglGetError() != EGL_SUCCESS) {
+ // TODO: Get the error properly
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_vaapi_textures: failed to bind egl image to gl texture, error: %d\n", self->egl->eglGetError());
+ self->egl->eglDestroyImage(self->egl->egl_display, image);
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+ return false;
+ }
+
+ self->egl->eglDestroyImage(self->egl->egl_display, image);
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+ }
+
+ gsr_color_conversion_params color_conversion_params = {0};
+ color_conversion_params.color_range = color_range;
+ color_conversion_params.egl = self->egl;
+ color_conversion_params.source_color = GSR_SOURCE_COLOR_RGB;
+ if(prime->fourcc == FOURCC_NV12)
+ color_conversion_params.destination_color = GSR_DESTINATION_COLOR_NV12;
+ else
+ color_conversion_params.destination_color = GSR_DESTINATION_COLOR_P010;
+
+ color_conversion_params.destination_textures[0] = self->target_textures[0];
+ color_conversion_params.destination_textures[1] = self->target_textures[1];
+ color_conversion_params.num_destination_textures = 2;
+
+ if(gsr_color_conversion_init(&self->color_conversion, &color_conversion_params) != 0) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_vaapi_textures: failed to create color conversion\n");
+ return false;
+ }
+
+ return true;
+ } else {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_vaapi_textures: unexpected fourcc %u for output drm fd, expected nv12 or p010\n", prime->fourcc);
+ return false;
+ }
+}
+
+static unsigned int gl_create_texture(gsr_egl *egl, int width, int height, int internal_format, unsigned int format) {
+ unsigned int texture_id = 0;
+ egl->glGenTextures(1, &texture_id);
+ egl->glBindTexture(GL_TEXTURE_2D, texture_id);
+ egl->glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height, 0, format, GL_UNSIGNED_BYTE, NULL);
+
+ egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+
+ egl->glBindTexture(GL_TEXTURE_2D, 0);
+ return texture_id;
+}
+
+static bool cuda_register_opengl_texture(gsr_cuda *cuda, CUgraphicsResource *cuda_graphics_resource, CUarray *mapped_array, unsigned int texture_id) {
+ CUresult res;
+ res = cuda->cuGraphicsGLRegisterImage(cuda_graphics_resource, texture_id, GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_NONE);
+ if (res != CUDA_SUCCESS) {
+ const char *err_str = "unknown";
+ cuda->cuGetErrorString(res, &err_str);
+ fprintf(stderr, "gsr error: cuda_register_opengl_texture: cuGraphicsGLRegisterImage failed, error: %s, texture " "id: %u\n", err_str, texture_id);
+ return false;
+ }
+
+ res = cuda->cuGraphicsResourceSetMapFlags(*cuda_graphics_resource, CU_GRAPHICS_MAP_RESOURCE_FLAGS_NONE);
+ res = cuda->cuGraphicsMapResources(1, cuda_graphics_resource, 0);
+
+ res = cuda->cuGraphicsSubResourceGetMappedArray(mapped_array, *cuda_graphics_resource, 0, 0);
+ return true;
+}
+
+bool gsr_capture_base_setup_cuda_textures(gsr_capture_base *self, AVFrame *frame, gsr_cuda_context *cuda_context, gsr_color_range color_range, gsr_source_color source_color, bool hdr) {
+ // TODO:
+ const int res = av_hwframe_get_buffer(self->video_codec_context->hw_frames_ctx, frame, 0);
+ if(res < 0) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_cuda_textures: av_hwframe_get_buffer failed: %d\n", res);
+ return false;
+ }
+
+ self->egl->glGenTextures(1, &self->input_texture);
+ self->egl->glBindTexture(GL_TEXTURE_2D, self->input_texture);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ self->egl->glGenTextures(1, &self->cursor_texture);
+ self->egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, self->cursor_texture);
+ self->egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ self->egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ self->egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
+
+ const unsigned int internal_formats_nv12[2] = { GL_R8, GL_RG8 };
+ const unsigned int internal_formats_p010[2] = { GL_R16, GL_RG16 };
+ const unsigned int formats[2] = { GL_RED, GL_RG };
+ const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size
+
+ for(int i = 0; i < 2; ++i) {
+ self->target_textures[i] = gl_create_texture(self->egl, self->video_codec_context->width / div[i], self->video_codec_context->height / div[i], !hdr ? internal_formats_nv12[i] : internal_formats_p010[i], formats[i]);
+ if(self->target_textures[i] == 0) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_cuda_textures: failed to create opengl texture\n");
+ return false;
+ }
+
+ if(!cuda_register_opengl_texture(cuda_context->cuda, &cuda_context->cuda_graphics_resources[i], &cuda_context->mapped_arrays[i], self->target_textures[i])) {
+ return false;
+ }
+ }
+
+ gsr_color_conversion_params color_conversion_params = {0};
+ color_conversion_params.color_range = color_range;
+ color_conversion_params.egl = self->egl;
+ color_conversion_params.source_color = source_color;
+ if(!hdr)
+ color_conversion_params.destination_color = GSR_DESTINATION_COLOR_NV12;
+ else
+ color_conversion_params.destination_color = GSR_DESTINATION_COLOR_P010;
+
+ color_conversion_params.destination_textures[0] = self->target_textures[0];
+ color_conversion_params.destination_textures[1] = self->target_textures[1];
+ color_conversion_params.num_destination_textures = 2;
+ color_conversion_params.load_external_image_shader = true;
+
+ if(gsr_color_conversion_init(&self->color_conversion, &color_conversion_params) != 0) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_setup_cuda_textures: failed to create color conversion\n");
+ return false;
+ }
+
+ return true;
+}
+
+void gsr_capture_base_stop(gsr_capture_base *self) {
+ gsr_color_conversion_deinit(&self->color_conversion);
+
+ if(self->egl->egl_context) {
+ if(self->input_texture) {
+ self->egl->glDeleteTextures(1, &self->input_texture);
+ self->input_texture = 0;
+ }
+
+ if(self->cursor_texture) {
+ self->egl->glDeleteTextures(1, &self->cursor_texture);
+ self->cursor_texture = 0;
+ }
+
+ self->egl->glDeleteTextures(2, self->target_textures);
+ self->target_textures[0] = 0;
+ self->target_textures[1] = 0;
+ }
+
+ if(self->video_codec_context->hw_device_ctx)
+ av_buffer_unref(&self->video_codec_context->hw_device_ctx);
+ if(self->video_codec_context->hw_frames_ctx)
+ av_buffer_unref(&self->video_codec_context->hw_frames_ctx);
+}
+
+bool drm_create_codec_context(const char *card_path, AVCodecContext *video_codec_context, int width, int height, bool hdr, VADisplay *va_dpy) {
+ char render_path[128];
+ if(!gsr_card_path_get_render_path(card_path, render_path)) {
+ fprintf(stderr, "gsr error: failed to get /dev/dri/renderDXXX file from %s\n", card_path);
+ return false;
+ }
+
+ AVBufferRef *device_ctx;
+ if(av_hwdevice_ctx_create(&device_ctx, AV_HWDEVICE_TYPE_VAAPI, render_path, NULL, 0) < 0) {
+ fprintf(stderr, "Error: Failed to create hardware device context\n");
+ return false;
+ }
+
+ AVBufferRef *frame_context = av_hwframe_ctx_alloc(device_ctx);
+ if(!frame_context) {
+ fprintf(stderr, "Error: Failed to create hwframe context\n");
+ av_buffer_unref(&device_ctx);
+ return false;
+ }
+
+ AVHWFramesContext *hw_frame_context =
+ (AVHWFramesContext *)frame_context->data;
+ hw_frame_context->width = width;
+ hw_frame_context->height = height;
+ hw_frame_context->sw_format = hdr ? AV_PIX_FMT_P010LE : AV_PIX_FMT_NV12;
+ hw_frame_context->format = video_codec_context->pix_fmt;
+ hw_frame_context->device_ref = device_ctx;
+ hw_frame_context->device_ctx = (AVHWDeviceContext*)device_ctx->data;
+
+ //hw_frame_context->initial_pool_size = 20;
+
+ AVVAAPIDeviceContext *vactx =((AVHWDeviceContext*)device_ctx->data)->hwctx;
+ *va_dpy = vactx->display;
+
+ if (av_hwframe_ctx_init(frame_context) < 0) {
+ fprintf(stderr, "Error: Failed to initialize hardware frame context "
+ "(note: ffmpeg version needs to be > 4.0)\n");
+ av_buffer_unref(&device_ctx);
+ //av_buffer_unref(&frame_context);
+ return false;
+ }
+
+ video_codec_context->hw_device_ctx = av_buffer_ref(device_ctx);
+ video_codec_context->hw_frames_ctx = av_buffer_ref(frame_context);
+ return true;
+}
+
+bool cuda_create_codec_context(CUcontext cu_ctx, AVCodecContext *video_codec_context, int width, int height, bool hdr, CUstream *cuda_stream) {
+ AVBufferRef *device_ctx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA);
+ if(!device_ctx) {
+ fprintf(stderr, "gsr error: cuda_create_codec_context failed: failed to create hardware device context\n");
+ return false;
+ }
+
+ AVHWDeviceContext *hw_device_context = (AVHWDeviceContext*)device_ctx->data;
+ AVCUDADeviceContext *cuda_device_context = (AVCUDADeviceContext*)hw_device_context->hwctx;
+ cuda_device_context->cuda_ctx = cu_ctx;
+ if(av_hwdevice_ctx_init(device_ctx) < 0) {
+ fprintf(stderr, "gsr error: cuda_create_codec_context failed: failed to create hardware device context\n");
+ av_buffer_unref(&device_ctx);
+ return false;
+ }
+
+ AVBufferRef *frame_context = av_hwframe_ctx_alloc(device_ctx);
+ if(!frame_context) {
+ fprintf(stderr, "gsr error: cuda_create_codec_context failed: failed to create hwframe context\n");
+ av_buffer_unref(&device_ctx);
+ return false;
+ }
+
+ AVHWFramesContext *hw_frame_context = (AVHWFramesContext*)frame_context->data;
+ hw_frame_context->width = width;
+ hw_frame_context->height = height;
+ hw_frame_context->sw_format = hdr ? AV_PIX_FMT_P010LE : AV_PIX_FMT_NV12;
+ hw_frame_context->format = video_codec_context->pix_fmt;
+ hw_frame_context->device_ref = device_ctx;
+ hw_frame_context->device_ctx = (AVHWDeviceContext*)device_ctx->data;
+
+ if (av_hwframe_ctx_init(frame_context) < 0) {
+ fprintf(stderr, "gsr error: cuda_create_codec_context failed: failed to initialize hardware frame context "
+ "(note: ffmpeg version needs to be > 4.0)\n");
+ av_buffer_unref(&device_ctx);
+ //av_buffer_unref(&frame_context);
+ return false;
+ }
+
+ *cuda_stream = cuda_device_context->stream;
+ video_codec_context->hw_device_ctx = av_buffer_ref(device_ctx);
+ video_codec_context->hw_frames_ctx = av_buffer_ref(frame_context);
+ return true;
+}
diff --git a/src/capture/kms.c b/src/capture/kms.c
new file mode 100644
index 0000000..16b20b7
--- /dev/null
+++ b/src/capture/kms.c
@@ -0,0 +1,369 @@
+#include "../../include/capture/kms.h"
+#include "../../include/capture/capture.h"
+#include "../../include/utils.h"
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <libavcodec/avcodec.h>
+#include <libavutil/mastering_display_metadata.h>
+
+#define HDMI_STATIC_METADATA_TYPE1 0
+#define HDMI_EOTF_SMPTE_ST2084 2
+
+/* TODO: On monitor reconfiguration, find monitor x, y, width and height again. Do the same for nvfbc. */
+
+typedef struct {
+ MonitorId *monitor_id;
+ const char *monitor_to_capture;
+ int monitor_to_capture_len;
+ int num_monitors;
+} MonitorCallbackUserdata;
+
+static void monitor_callback(const gsr_monitor *monitor, void *userdata) {
+ MonitorCallbackUserdata *monitor_callback_userdata = userdata;
+ ++monitor_callback_userdata->num_monitors;
+
+ if(monitor_callback_userdata->monitor_to_capture_len != monitor->name_len || memcmp(monitor_callback_userdata->monitor_to_capture, monitor->name, monitor->name_len) != 0)
+ return;
+
+ if(monitor_callback_userdata->monitor_id->num_connector_ids < MAX_CONNECTOR_IDS) {
+ monitor_callback_userdata->monitor_id->connector_ids[monitor_callback_userdata->monitor_id->num_connector_ids] = monitor->connector_id;
+ ++monitor_callback_userdata->monitor_id->num_connector_ids;
+ }
+
+ if(monitor_callback_userdata->monitor_id->num_connector_ids == MAX_CONNECTOR_IDS)
+ fprintf(stderr, "gsr warning: reached max connector ids\n");
+}
+
+static int max_int(int a, int b) {
+ return a > b ? a : b;
+}
+
+int gsr_capture_kms_start(gsr_capture_kms *self, const char *display_to_capture, gsr_egl *egl, AVCodecContext *video_codec_context, AVFrame *frame) {
+ memset(self, 0, sizeof(*self));
+ self->base.video_codec_context = video_codec_context;
+ self->base.egl = egl;
+
+ gsr_monitor monitor;
+ self->monitor_id.num_connector_ids = 0;
+
+ int kms_init_res = gsr_kms_client_init(&self->kms_client, egl->card_path);
+ if(kms_init_res != 0)
+ return kms_init_res;
+
+ MonitorCallbackUserdata monitor_callback_userdata = {
+ &self->monitor_id,
+ display_to_capture, strlen(display_to_capture),
+ 0,
+ };
+ for_each_active_monitor_output(egl, GSR_CONNECTION_DRM, monitor_callback, &monitor_callback_userdata);
+
+ if(!get_monitor_by_name(egl, GSR_CONNECTION_DRM, display_to_capture, &monitor)) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_start: failed to find monitor by name \"%s\"\n", display_to_capture);
+ return -1;
+ }
+
+ monitor.name = display_to_capture;
+ self->monitor_rotation = drm_monitor_get_display_server_rotation(egl, &monitor);
+
+ self->capture_pos = monitor.pos;
+ if(self->monitor_rotation == GSR_MONITOR_ROT_90 || self->monitor_rotation == GSR_MONITOR_ROT_270) {
+ self->capture_size.x = monitor.size.y;
+ self->capture_size.y = monitor.size.x;
+ } else {
+ self->capture_size = monitor.size;
+ }
+
+ /* Disable vsync */
+ egl->eglSwapInterval(egl->egl_display, 0);
+
+ self->base.video_codec_context->width = max_int(2, even_number_ceil(self->capture_size.x));
+ self->base.video_codec_context->height = max_int(2, even_number_ceil(self->capture_size.y));
+
+ frame->width = self->base.video_codec_context->width;
+ frame->height = self->base.video_codec_context->height;
+ return 0;
+}
+
+void gsr_capture_kms_stop(gsr_capture_kms *self) {
+ gsr_capture_kms_cleanup_kms_fds(self);
+ gsr_kms_client_deinit(&self->kms_client);
+ gsr_capture_base_stop(&self->base);
+}
+
+static float monitor_rotation_to_radians(gsr_monitor_rotation rot) {
+ switch(rot) {
+ case GSR_MONITOR_ROT_0: return 0.0f;
+ case GSR_MONITOR_ROT_90: return M_PI_2;
+ case GSR_MONITOR_ROT_180: return M_PI;
+ case GSR_MONITOR_ROT_270: return M_PI + M_PI_2;
+ }
+ return 0.0f;
+}
+
+/* Prefer non combined planes */
+static gsr_kms_response_fd* find_drm_by_connector_id(gsr_kms_response *kms_response, uint32_t connector_id) {
+ int index_combined = -1;
+ for(int i = 0; i < kms_response->num_fds; ++i) {
+ if(kms_response->fds[i].connector_id == connector_id && !kms_response->fds[i].is_cursor) {
+ if(kms_response->fds[i].is_combined_plane)
+ index_combined = i;
+ else
+ return &kms_response->fds[i];
+ }
+ }
+
+ if(index_combined != -1)
+ return &kms_response->fds[index_combined];
+ else
+ return NULL;
+}
+
+static gsr_kms_response_fd* find_first_combined_drm(gsr_kms_response *kms_response) {
+ for(int i = 0; i < kms_response->num_fds; ++i) {
+ if(kms_response->fds[i].is_combined_plane && !kms_response->fds[i].is_cursor)
+ return &kms_response->fds[i];
+ }
+ return NULL;
+}
+
+static gsr_kms_response_fd* find_largest_drm(gsr_kms_response *kms_response) {
+ if(kms_response->num_fds == 0)
+ return NULL;
+
+ int64_t largest_size = 0;
+ gsr_kms_response_fd *largest_drm = &kms_response->fds[0];
+ for(int i = 0; i < kms_response->num_fds; ++i) {
+ const int64_t size = (int64_t)kms_response->fds[i].width * (int64_t)kms_response->fds[i].height;
+ if(size > largest_size && !kms_response->fds[i].is_cursor) {
+ largest_size = size;
+ largest_drm = &kms_response->fds[i];
+ }
+ }
+ return largest_drm;
+}
+
+static gsr_kms_response_fd* find_cursor_drm(gsr_kms_response *kms_response) {
+ for(int i = 0; i < kms_response->num_fds; ++i) {
+ if(kms_response->fds[i].is_cursor)
+ return &kms_response->fds[i];
+ }
+ return NULL;
+}
+
+static bool hdr_metadata_is_supported_format(const struct hdr_output_metadata *hdr_metadata) {
+ return hdr_metadata->metadata_type == HDMI_STATIC_METADATA_TYPE1 &&
+ hdr_metadata->hdmi_metadata_type1.metadata_type == HDMI_STATIC_METADATA_TYPE1 &&
+ hdr_metadata->hdmi_metadata_type1.eotf == HDMI_EOTF_SMPTE_ST2084;
+}
+
+static void gsr_kms_set_hdr_metadata(gsr_capture_kms *self, AVFrame *frame, gsr_kms_response_fd *drm_fd) {
+ if(!self->mastering_display_metadata)
+ self->mastering_display_metadata = av_mastering_display_metadata_create_side_data(frame);
+
+ if(!self->light_metadata)
+ self->light_metadata = av_content_light_metadata_create_side_data(frame);
+
+ if(self->mastering_display_metadata) {
+ for(int i = 0; i < 3; ++i) {
+ self->mastering_display_metadata->display_primaries[i][0] = av_make_q(drm_fd->hdr_metadata.hdmi_metadata_type1.display_primaries[i].x, 50000);
+ self->mastering_display_metadata->display_primaries[i][1] = av_make_q(drm_fd->hdr_metadata.hdmi_metadata_type1.display_primaries[i].y, 50000);
+ }
+
+ self->mastering_display_metadata->white_point[0] = av_make_q(drm_fd->hdr_metadata.hdmi_metadata_type1.white_point.x, 50000);
+ self->mastering_display_metadata->white_point[1] = av_make_q(drm_fd->hdr_metadata.hdmi_metadata_type1.white_point.y, 50000);
+
+ self->mastering_display_metadata->min_luminance = av_make_q(drm_fd->hdr_metadata.hdmi_metadata_type1.min_display_mastering_luminance, 10000);
+ self->mastering_display_metadata->max_luminance = av_make_q(drm_fd->hdr_metadata.hdmi_metadata_type1.max_display_mastering_luminance, 1);
+
+ self->mastering_display_metadata->has_primaries = self->mastering_display_metadata->display_primaries[0][0].num > 0;
+ self->mastering_display_metadata->has_luminance = self->mastering_display_metadata->max_luminance.num > 0;
+ }
+
+ if(self->light_metadata) {
+ self->light_metadata->MaxCLL = drm_fd->hdr_metadata.hdmi_metadata_type1.max_cll;
+ self->light_metadata->MaxFALL = drm_fd->hdr_metadata.hdmi_metadata_type1.max_fall;
+ }
+}
+
+static vec2i swap_vec2i(vec2i value) {
+ int tmp = value.x;
+ value.x = value.y;
+ value.y = tmp;
+ return value;
+}
+
+bool gsr_capture_kms_capture(gsr_capture_kms *self, AVFrame *frame, bool hdr, bool screen_plane_use_modifiers, bool cursor_texture_is_external, bool record_cursor) {
+ //egl->glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ self->base.egl->glClear(0);
+
+ gsr_capture_kms_cleanup_kms_fds(self);
+
+ gsr_kms_response_fd *drm_fd = NULL;
+ gsr_kms_response_fd *cursor_drm_fd = NULL;
+ bool capture_is_combined_plane = false;
+
+ if(gsr_kms_client_get_kms(&self->kms_client, &self->kms_response) != 0) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_capture: failed to get kms, error: %d (%s)\n", self->kms_response.result, self->kms_response.err_msg);
+ return false;
+ }
+
+ if(self->kms_response.num_fds == 0) {
+ static bool error_shown = false;
+ if(!error_shown) {
+ error_shown = true;
+ fprintf(stderr, "gsr error: no drm found, capture will fail\n");
+ }
+ return false;
+ }
+
+ for(int i = 0; i < self->monitor_id.num_connector_ids; ++i) {
+ drm_fd = find_drm_by_connector_id(&self->kms_response, self->monitor_id.connector_ids[i]);
+ if(drm_fd)
+ break;
+ }
+
+ // Will never happen on wayland unless the target monitor has been disconnected
+ if(!drm_fd) {
+ drm_fd = find_first_combined_drm(&self->kms_response);
+ if(!drm_fd)
+ drm_fd = find_largest_drm(&self->kms_response);
+ capture_is_combined_plane = true;
+ }
+
+ cursor_drm_fd = find_cursor_drm(&self->kms_response);
+
+ if(!drm_fd)
+ return false;
+
+ if(!capture_is_combined_plane && cursor_drm_fd && cursor_drm_fd->connector_id != drm_fd->connector_id)
+ cursor_drm_fd = NULL;
+
+ if(drm_fd->has_hdr_metadata && hdr && hdr_metadata_is_supported_format(&drm_fd->hdr_metadata))
+ gsr_kms_set_hdr_metadata(self, frame, drm_fd);
+
+ // TODO: This causes a crash sometimes on steam deck, why? is it a driver bug? a vaapi pure version doesn't cause a crash.
+ // Even ffmpeg kmsgrab causes this crash. The error is:
+ // amdgpu: Failed to allocate a buffer:
+ // amdgpu: size : 28508160 bytes
+ // amdgpu: alignment : 2097152 bytes
+ // amdgpu: domains : 4
+ // amdgpu: flags : 4
+ // amdgpu: Failed to allocate a buffer:
+ // amdgpu: size : 28508160 bytes
+ // amdgpu: alignment : 2097152 bytes
+ // amdgpu: domains : 4
+ // amdgpu: flags : 4
+ // EE ../jupiter-mesa/src/gallium/drivers/radeonsi/radeon_vcn_enc.c:516 radeon_create_encoder UVD - Can't create CPB buffer.
+ // [hevc_vaapi @ 0x55ea72b09840] Failed to upload encode parameters: 2 (resource allocation failed).
+ // [hevc_vaapi @ 0x55ea72b09840] Encode failed: -5.
+ // Error: avcodec_send_frame failed, error: Input/output error
+ // Assertion pic->display_order == pic->encode_order failed at libavcodec/vaapi_encode_h265.c:765
+ // kms server info: kms client shutdown, shutting down the server
+ intptr_t img_attr[18] = {
+ EGL_LINUX_DRM_FOURCC_EXT, drm_fd->pixel_format,
+ EGL_WIDTH, drm_fd->width,
+ EGL_HEIGHT, drm_fd->height,
+ EGL_DMA_BUF_PLANE0_FD_EXT, drm_fd->fd,
+ EGL_DMA_BUF_PLANE0_OFFSET_EXT, drm_fd->offset,
+ EGL_DMA_BUF_PLANE0_PITCH_EXT, drm_fd->pitch,
+ };
+
+ if(screen_plane_use_modifiers) {
+ img_attr[12] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT;
+ img_attr[13] = drm_fd->modifier & 0xFFFFFFFFULL;
+
+ img_attr[14] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT;
+ img_attr[15] = drm_fd->modifier >> 32ULL;
+
+ img_attr[16] = EGL_NONE;
+ img_attr[17] = EGL_NONE;
+ } else {
+ img_attr[12] = EGL_NONE;
+ img_attr[13] = EGL_NONE;
+ }
+
+ EGLImage image = self->base.egl->eglCreateImage(self->base.egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, img_attr);
+ self->base.egl->glBindTexture(GL_TEXTURE_2D, self->base.input_texture);
+ self->base.egl->glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image);
+ self->base.egl->eglDestroyImage(self->base.egl->egl_display, image);
+ self->base.egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ vec2i capture_pos = self->capture_pos;
+ if(!capture_is_combined_plane)
+ capture_pos = (vec2i){drm_fd->x, drm_fd->y};
+
+ const float texture_rotation = monitor_rotation_to_radians(self->monitor_rotation);
+
+ gsr_color_conversion_draw(&self->base.color_conversion, self->base.input_texture,
+ (vec2i){0, 0}, self->capture_size,
+ capture_pos, self->capture_size,
+ texture_rotation, false);
+
+ if(record_cursor && cursor_drm_fd) {
+ const vec2i cursor_size = {cursor_drm_fd->width, cursor_drm_fd->height};
+ vec2i cursor_pos = {cursor_drm_fd->x, cursor_drm_fd->y};
+ switch(self->monitor_rotation) {
+ case GSR_MONITOR_ROT_0:
+ break;
+ case GSR_MONITOR_ROT_90:
+ cursor_pos = swap_vec2i(cursor_pos);
+ cursor_pos.x = self->capture_size.x - cursor_pos.x;
+ // TODO: Remove this horrible hack
+ cursor_pos.x -= cursor_size.x;
+ break;
+ case GSR_MONITOR_ROT_180:
+ cursor_pos.x = self->capture_size.x - cursor_pos.x;
+ cursor_pos.y = self->capture_size.y - cursor_pos.y;
+ // TODO: Remove this horrible hack
+ cursor_pos.x -= cursor_size.x;
+ cursor_pos.y -= cursor_size.y;
+ break;
+ case GSR_MONITOR_ROT_270:
+ cursor_pos = swap_vec2i(cursor_pos);
+ cursor_pos.y = self->capture_size.y - cursor_pos.y;
+ // TODO: Remove this horrible hack
+ cursor_pos.y -= cursor_size.y;
+ break;
+ }
+
+ const intptr_t img_attr_cursor[] = {
+ EGL_LINUX_DRM_FOURCC_EXT, cursor_drm_fd->pixel_format,
+ EGL_WIDTH, cursor_drm_fd->width,
+ EGL_HEIGHT, cursor_drm_fd->height,
+ EGL_DMA_BUF_PLANE0_FD_EXT, cursor_drm_fd->fd,
+ EGL_DMA_BUF_PLANE0_OFFSET_EXT, cursor_drm_fd->offset,
+ EGL_DMA_BUF_PLANE0_PITCH_EXT, cursor_drm_fd->pitch,
+ EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, cursor_drm_fd->modifier & 0xFFFFFFFFULL,
+ EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, cursor_drm_fd->modifier >> 32ULL,
+ EGL_NONE
+ };
+
+ EGLImage cursor_image = self->base.egl->eglCreateImage(self->base.egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, img_attr_cursor);
+ const int target = cursor_texture_is_external ? GL_TEXTURE_EXTERNAL_OES : GL_TEXTURE_2D;
+ self->base.egl->glBindTexture(target, self->base.cursor_texture);
+ self->base.egl->glEGLImageTargetTexture2DOES(target, cursor_image);
+ self->base.egl->eglDestroyImage(self->base.egl->egl_display, cursor_image);
+ self->base.egl->glBindTexture(target, 0);
+
+ gsr_color_conversion_draw(&self->base.color_conversion, self->base.cursor_texture,
+ cursor_pos, cursor_size,
+ (vec2i){0, 0}, cursor_size,
+ texture_rotation, cursor_texture_is_external);
+ }
+
+ self->base.egl->eglSwapBuffers(self->base.egl->egl_display, self->base.egl->egl_surface);
+ //self->base.egl->glFlush();
+ //self->base.egl->glFinish();
+
+ return true;
+}
+
+void gsr_capture_kms_cleanup_kms_fds(gsr_capture_kms *self) {
+ for(int i = 0; i < self->kms_response.num_fds; ++i) {
+ if(self->kms_response.fds[i].fd > 0)
+ close(self->kms_response.fds[i].fd);
+ self->kms_response.fds[i].fd = 0;
+ }
+ self->kms_response.num_fds = 0;
+}
diff --git a/src/capture/kms_cuda.c b/src/capture/kms_cuda.c
new file mode 100644
index 0000000..a9f1f8e
--- /dev/null
+++ b/src/capture/kms_cuda.c
@@ -0,0 +1,181 @@
+#include "../../include/capture/kms_cuda.h"
+#include "../../include/capture/kms.h"
+#include "../../include/cuda.h"
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <assert.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_cuda.h>
+#include <libavcodec/avcodec.h>
+
+typedef struct {
+ gsr_capture_kms kms;
+
+ gsr_capture_kms_cuda_params params;
+
+ gsr_cuda cuda;
+ CUgraphicsResource cuda_graphics_resources[2];
+ CUarray mapped_arrays[2];
+ CUstream cuda_stream;
+} gsr_capture_kms_cuda;
+
+static void gsr_capture_kms_cuda_stop(gsr_capture *cap, AVCodecContext *video_codec_context);
+
+static int gsr_capture_kms_cuda_start(gsr_capture *cap, AVCodecContext *video_codec_context, AVFrame *frame) {
+ gsr_capture_kms_cuda *cap_kms = cap->priv;
+
+ const int res = gsr_capture_kms_start(&cap_kms->kms, cap_kms->params.display_to_capture, cap_kms->params.egl, video_codec_context, frame);
+ if(res != 0) {
+ gsr_capture_kms_cuda_stop(cap, video_codec_context);
+ return res;
+ }
+
+ // TODO: overclocking is not supported on wayland...
+ if(!gsr_cuda_load(&cap_kms->cuda, NULL, false)) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_cuda_start: failed to load cuda\n");
+ gsr_capture_kms_cuda_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ if(!cuda_create_codec_context(cap_kms->cuda.cu_ctx, video_codec_context, video_codec_context->width, video_codec_context->height, cap_kms->params.hdr, &cap_kms->cuda_stream)) {
+ gsr_capture_kms_cuda_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ gsr_cuda_context cuda_context = {
+ .cuda = &cap_kms->cuda,
+ .cuda_graphics_resources = cap_kms->cuda_graphics_resources,
+ .mapped_arrays = cap_kms->mapped_arrays
+ };
+
+ if(!gsr_capture_base_setup_cuda_textures(&cap_kms->kms.base, frame, &cuda_context, cap_kms->params.color_range, GSR_SOURCE_COLOR_RGB, cap_kms->params.hdr)) {
+ gsr_capture_kms_cuda_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ return 0;
+}
+
+static bool gsr_capture_kms_cuda_should_stop(gsr_capture *cap, bool *err) {
+ gsr_capture_kms_cuda *cap_kms = cap->priv;
+ if(cap_kms->kms.should_stop) {
+ if(err)
+ *err = cap_kms->kms.stop_is_error;
+ return true;
+ }
+
+ if(err)
+ *err = false;
+ return false;
+}
+
+static void gsr_capture_kms_unload_cuda_graphics(gsr_capture_kms_cuda *cap_kms) {
+ if(cap_kms->cuda.cu_ctx) {
+ for(int i = 0; i < 2; ++i) {
+ if(cap_kms->cuda_graphics_resources[i]) {
+ cap_kms->cuda.cuGraphicsUnmapResources(1, &cap_kms->cuda_graphics_resources[i], 0);
+ cap_kms->cuda.cuGraphicsUnregisterResource(cap_kms->cuda_graphics_resources[i]);
+ cap_kms->cuda_graphics_resources[i] = 0;
+ }
+ }
+ }
+}
+
+static int gsr_capture_kms_cuda_capture(gsr_capture *cap, AVFrame *frame) {
+ gsr_capture_kms_cuda *cap_kms = cap->priv;
+
+ gsr_capture_kms_capture(&cap_kms->kms, frame, cap_kms->params.hdr, true, true, cap_kms->params.record_cursor);
+
+ const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size
+ for(int i = 0; i < 2; ++i) {
+ CUDA_MEMCPY2D memcpy_struct;
+ memcpy_struct.srcXInBytes = 0;
+ memcpy_struct.srcY = 0;
+ memcpy_struct.srcMemoryType = CU_MEMORYTYPE_ARRAY;
+
+ memcpy_struct.dstXInBytes = 0;
+ memcpy_struct.dstY = 0;
+ memcpy_struct.dstMemoryType = CU_MEMORYTYPE_DEVICE;
+
+ memcpy_struct.srcArray = cap_kms->mapped_arrays[i];
+ memcpy_struct.srcPitch = frame->width / div[i];
+ memcpy_struct.dstDevice = (CUdeviceptr)frame->data[i];
+ memcpy_struct.dstPitch = frame->linesize[i];
+ memcpy_struct.WidthInBytes = frame->width * (cap_kms->params.hdr ? 2 : 1);
+ memcpy_struct.Height = frame->height / div[i];
+ // TODO: Remove this copy if possible
+ cap_kms->cuda.cuMemcpy2DAsync_v2(&memcpy_struct, cap_kms->cuda_stream);
+ }
+
+ // TODO: needed?
+ cap_kms->cuda.cuStreamSynchronize(cap_kms->cuda_stream);
+
+ return 0;
+}
+
+static void gsr_capture_kms_cuda_capture_end(gsr_capture *cap, AVFrame *frame) {
+ (void)frame;
+ gsr_capture_kms_cuda *cap_kms = cap->priv;
+ gsr_capture_kms_cleanup_kms_fds(&cap_kms->kms);
+}
+
+static void gsr_capture_kms_cuda_stop(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ gsr_capture_kms_cuda *cap_kms = cap->priv;
+ gsr_capture_kms_unload_cuda_graphics(cap_kms);
+ gsr_cuda_unload(&cap_kms->cuda);
+ gsr_capture_kms_stop(&cap_kms->kms);
+}
+
+static void gsr_capture_kms_cuda_destroy(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ gsr_capture_kms_cuda *cap_kms = cap->priv;
+ if(cap->priv) {
+ gsr_capture_kms_cuda_stop(cap, video_codec_context);
+ free((void*)cap_kms->params.display_to_capture);
+ cap_kms->params.display_to_capture = NULL;
+ free(cap->priv);
+ cap->priv = NULL;
+ }
+ free(cap);
+}
+
+gsr_capture* gsr_capture_kms_cuda_create(const gsr_capture_kms_cuda_params *params) {
+ if(!params) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_cuda_create params is NULL\n");
+ return NULL;
+ }
+
+ gsr_capture *cap = calloc(1, sizeof(gsr_capture));
+ if(!cap)
+ return NULL;
+
+ gsr_capture_kms_cuda *cap_kms = calloc(1, sizeof(gsr_capture_kms_cuda));
+ if(!cap_kms) {
+ free(cap);
+ return NULL;
+ }
+
+ const char *display_to_capture = strdup(params->display_to_capture);
+ if(!display_to_capture) {
+ free(cap);
+ free(cap_kms);
+ return NULL;
+ }
+
+ cap_kms->params = *params;
+ cap_kms->params.display_to_capture = display_to_capture;
+
+ *cap = (gsr_capture) {
+ .start = gsr_capture_kms_cuda_start,
+ .tick = NULL,
+ .should_stop = gsr_capture_kms_cuda_should_stop,
+ .capture = gsr_capture_kms_cuda_capture,
+ .capture_end = gsr_capture_kms_cuda_capture_end,
+ .destroy = gsr_capture_kms_cuda_destroy,
+ .priv = cap_kms
+ };
+
+ return cap;
+}
diff --git a/src/capture/kms_vaapi.c b/src/capture/kms_vaapi.c
new file mode 100644
index 0000000..a7e8182
--- /dev/null
+++ b/src/capture/kms_vaapi.c
@@ -0,0 +1,135 @@
+#include "../../include/capture/kms_vaapi.h"
+#include "../../include/capture/kms.h"
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <assert.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_vaapi.h>
+#include <libavcodec/avcodec.h>
+#include <va/va_drmcommon.h>
+
+typedef struct {
+ gsr_capture_kms kms;
+
+ gsr_capture_kms_vaapi_params params;
+
+ VADisplay va_dpy;
+ VADRMPRIMESurfaceDescriptor prime;
+} gsr_capture_kms_vaapi;
+
+static void gsr_capture_kms_vaapi_stop(gsr_capture *cap, AVCodecContext *video_codec_context);
+
+static int gsr_capture_kms_vaapi_start(gsr_capture *cap, AVCodecContext *video_codec_context, AVFrame *frame) {
+ gsr_capture_kms_vaapi *cap_kms = cap->priv;
+
+ int res = gsr_capture_kms_start(&cap_kms->kms, cap_kms->params.display_to_capture, cap_kms->params.egl, video_codec_context, frame);
+ if(res != 0) {
+ gsr_capture_kms_vaapi_stop(cap, video_codec_context);
+ return res;
+ }
+
+ if(!drm_create_codec_context(cap_kms->params.egl->card_path, video_codec_context, video_codec_context->width, video_codec_context->height, cap_kms->params.hdr, &cap_kms->va_dpy)) {
+ gsr_capture_kms_vaapi_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ if(!gsr_capture_base_setup_vaapi_textures(&cap_kms->kms.base, frame, cap_kms->va_dpy, &cap_kms->prime, cap_kms->params.color_range)) {
+ gsr_capture_kms_vaapi_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ return 0;
+}
+
+static bool gsr_capture_kms_vaapi_should_stop(gsr_capture *cap, bool *err) {
+ gsr_capture_kms_vaapi *cap_kms = cap->priv;
+ if(cap_kms->kms.should_stop) {
+ if(err)
+ *err = cap_kms->kms.stop_is_error;
+ return true;
+ }
+
+ if(err)
+ *err = false;
+ return false;
+}
+
+static int gsr_capture_kms_vaapi_capture(gsr_capture *cap, AVFrame *frame) {
+ gsr_capture_kms_vaapi *cap_kms = cap->priv;
+ gsr_capture_kms_capture(&cap_kms->kms, frame, cap_kms->params.hdr, false, false, cap_kms->params.record_cursor);
+ return 0;
+}
+
+static void gsr_capture_kms_vaapi_capture_end(gsr_capture *cap, AVFrame *frame) {
+ (void)frame;
+ gsr_capture_kms_vaapi *cap_kms = cap->priv;
+ gsr_capture_kms_cleanup_kms_fds(&cap_kms->kms);
+}
+
+static void gsr_capture_kms_vaapi_stop(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ gsr_capture_kms_vaapi *cap_kms = cap->priv;
+
+ for(uint32_t i = 0; i < cap_kms->prime.num_objects; ++i) {
+ if(cap_kms->prime.objects[i].fd > 0) {
+ close(cap_kms->prime.objects[i].fd);
+ cap_kms->prime.objects[i].fd = 0;
+ }
+ }
+
+ gsr_capture_kms_stop(&cap_kms->kms);
+}
+
+static void gsr_capture_kms_vaapi_destroy(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ gsr_capture_kms_vaapi *cap_kms = cap->priv;
+ if(cap->priv) {
+ gsr_capture_kms_vaapi_stop(cap, video_codec_context);
+ free((void*)cap_kms->params.display_to_capture);
+ cap_kms->params.display_to_capture = NULL;
+ free(cap->priv);
+ cap->priv = NULL;
+ }
+ free(cap);
+}
+
+gsr_capture* gsr_capture_kms_vaapi_create(const gsr_capture_kms_vaapi_params *params) {
+ if(!params) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_vaapi_create params is NULL\n");
+ return NULL;
+ }
+
+ gsr_capture *cap = calloc(1, sizeof(gsr_capture));
+ if(!cap)
+ return NULL;
+
+ gsr_capture_kms_vaapi *cap_kms = calloc(1, sizeof(gsr_capture_kms_vaapi));
+ if(!cap_kms) {
+ free(cap);
+ return NULL;
+ }
+
+ const char *display_to_capture = strdup(params->display_to_capture);
+ if(!display_to_capture) {
+ /* TODO XCloseDisplay */
+ free(cap);
+ free(cap_kms);
+ return NULL;
+ }
+
+ cap_kms->params = *params;
+ cap_kms->params.display_to_capture = display_to_capture;
+
+ *cap = (gsr_capture) {
+ .start = gsr_capture_kms_vaapi_start,
+ .tick = NULL,
+ .should_stop = gsr_capture_kms_vaapi_should_stop,
+ .capture = gsr_capture_kms_vaapi_capture,
+ .capture_end = gsr_capture_kms_vaapi_capture_end,
+ .destroy = gsr_capture_kms_vaapi_destroy,
+ .priv = cap_kms
+ };
+
+ return cap;
+}
diff --git a/src/capture/nvfbc.c b/src/capture/nvfbc.c
new file mode 100644
index 0000000..9eabb18
--- /dev/null
+++ b/src/capture/nvfbc.c
@@ -0,0 +1,535 @@
+#include "../../include/capture/nvfbc.h"
+#include "../../external/NvFBC.h"
+#include "../../include/cuda.h"
+#include "../../include/egl.h"
+#include "../../include/utils.h"
+#include <dlfcn.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+#include <X11/Xlib.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext_cuda.h>
+#include <libavutil/frame.h>
+#include <libavutil/version.h>
+#include <libavcodec/avcodec.h>
+
+typedef struct {
+ gsr_capture_base base;
+ gsr_capture_nvfbc_params params;
+ void *library;
+
+ NVFBC_SESSION_HANDLE nv_fbc_handle;
+ PNVFBCCREATEINSTANCE nv_fbc_create_instance;
+ NVFBC_API_FUNCTION_LIST nv_fbc_function_list;
+ bool fbc_handle_created;
+ bool capture_session_created;
+
+ gsr_cuda cuda;
+ CUgraphicsResource cuda_graphics_resources[2];
+ CUarray mapped_arrays[2];
+ CUstream cuda_stream; // TODO: asdasdsa
+ NVFBC_TOGL_SETUP_PARAMS setup_params;
+
+ bool direct_capture;
+ bool supports_direct_cursor;
+ bool capture_region;
+ uint32_t x, y, width, height;
+ NVFBC_TRACKING_TYPE tracking_type;
+ uint32_t output_id;
+ uint32_t tracking_width, tracking_height;
+ bool nvfbc_needs_recreate;
+ double nvfbc_dead_start;
+} gsr_capture_nvfbc;
+
+#if defined(_WIN64) || defined(__LP64__)
+typedef unsigned long long CUdeviceptr_v2;
+#else
+typedef unsigned int CUdeviceptr_v2;
+#endif
+typedef CUdeviceptr_v2 CUdeviceptr;
+
+static int max_int(int a, int b) {
+ return a > b ? a : b;
+}
+
+/* Returns 0 on failure */
+static uint32_t get_output_id_from_display_name(NVFBC_RANDR_OUTPUT_INFO *outputs, uint32_t num_outputs, const char *display_name, uint32_t *width, uint32_t *height) {
+ if(!outputs)
+ return 0;
+
+ for(uint32_t i = 0; i < num_outputs; ++i) {
+ if(strcmp(outputs[i].name, display_name) == 0) {
+ *width = outputs[i].trackedBox.w;
+ *height = outputs[i].trackedBox.h;
+ return outputs[i].dwId;
+ }
+ }
+
+ return 0;
+}
+
+/* TODO: Test with optimus and open kernel modules */
+static bool get_driver_version(int *major, int *minor) {
+ *major = 0;
+ *minor = 0;
+
+ FILE *f = fopen("/proc/driver/nvidia/version", "rb");
+ if(!f) {
+ fprintf(stderr, "gsr warning: failed to get nvidia driver version (failed to read /proc/driver/nvidia/version)\n");
+ return false;
+ }
+
+ char buffer[2048];
+ size_t bytes_read = fread(buffer, 1, sizeof(buffer) - 1, f);
+ buffer[bytes_read] = '\0';
+
+ bool success = false;
+ const char *p = strstr(buffer, "Kernel Module");
+ if(p) {
+ p += 13;
+ int driver_major_version = 0, driver_minor_version = 0;
+ if(sscanf(p, "%d.%d", &driver_major_version, &driver_minor_version) == 2) {
+ *major = driver_major_version;
+ *minor = driver_minor_version;
+ success = true;
+ }
+ }
+
+ if(!success)
+ fprintf(stderr, "gsr warning: failed to get nvidia driver version\n");
+
+ fclose(f);
+ return success;
+}
+
+static bool version_at_least(int major, int minor, int expected_major, int expected_minor) {
+ return major > expected_major || (major == expected_major && minor >= expected_minor);
+}
+
+static bool version_less_than(int major, int minor, int expected_major, int expected_minor) {
+ return major < expected_major || (major == expected_major && minor < expected_minor);
+}
+
+static void set_func_ptr(void **dst, void *src) {
+ *dst = src;
+}
+
+static bool gsr_capture_nvfbc_load_library(gsr_capture *cap) {
+ gsr_capture_nvfbc *cap_nvfbc = cap->priv;
+
+ dlerror(); /* clear */
+ void *lib = dlopen("libnvidia-fbc.so.1", RTLD_LAZY);
+ if(!lib) {
+ fprintf(stderr, "gsr error: failed to load libnvidia-fbc.so.1, error: %s\n", dlerror());
+ return false;
+ }
+
+ set_func_ptr((void**)&cap_nvfbc->nv_fbc_create_instance, dlsym(lib, "NvFBCCreateInstance"));
+ if(!cap_nvfbc->nv_fbc_create_instance) {
+ fprintf(stderr, "gsr error: unable to resolve symbol 'NvFBCCreateInstance'\n");
+ dlclose(lib);
+ return false;
+ }
+
+ memset(&cap_nvfbc->nv_fbc_function_list, 0, sizeof(cap_nvfbc->nv_fbc_function_list));
+ cap_nvfbc->nv_fbc_function_list.dwVersion = NVFBC_VERSION;
+ NVFBCSTATUS status = cap_nvfbc->nv_fbc_create_instance(&cap_nvfbc->nv_fbc_function_list);
+ if(status != NVFBC_SUCCESS) {
+ fprintf(stderr, "gsr error: failed to create NvFBC instance (status: %d)\n", status);
+ dlclose(lib);
+ return false;
+ }
+
+ cap_nvfbc->library = lib;
+ return true;
+}
+
+/* TODO: check for glx swap control extension string (GLX_EXT_swap_control, etc) */
+static void set_vertical_sync_enabled(gsr_egl *egl, int enabled) {
+ int result = 0;
+
+ if(egl->glXSwapIntervalEXT) {
+ egl->glXSwapIntervalEXT(egl->x11.dpy, egl->x11.window, enabled ? 1 : 0);
+ } else if(egl->glXSwapIntervalMESA) {
+ result = egl->glXSwapIntervalMESA(enabled ? 1 : 0);
+ } else if(egl->glXSwapIntervalSGI) {
+ result = egl->glXSwapIntervalSGI(enabled ? 1 : 0);
+ } else {
+ static int warned = 0;
+ if (!warned) {
+ warned = 1;
+ fprintf(stderr, "gsr warning: setting vertical sync not supported\n");
+ }
+ }
+
+ if(result != 0)
+ fprintf(stderr, "gsr warning: setting vertical sync failed\n");
+}
+
+static void gsr_capture_nvfbc_destroy_session(gsr_capture_nvfbc *cap_nvfbc) {
+ if(cap_nvfbc->fbc_handle_created && cap_nvfbc->capture_session_created) {
+ NVFBC_DESTROY_CAPTURE_SESSION_PARAMS destroy_capture_params;
+ memset(&destroy_capture_params, 0, sizeof(destroy_capture_params));
+ destroy_capture_params.dwVersion = NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER;
+ cap_nvfbc->nv_fbc_function_list.nvFBCDestroyCaptureSession(cap_nvfbc->nv_fbc_handle, &destroy_capture_params);
+ cap_nvfbc->capture_session_created = false;
+ }
+}
+
+static void gsr_capture_nvfbc_destroy_handle(gsr_capture_nvfbc *cap_nvfbc) {
+ if(cap_nvfbc->fbc_handle_created) {
+ NVFBC_DESTROY_HANDLE_PARAMS destroy_params;
+ memset(&destroy_params, 0, sizeof(destroy_params));
+ destroy_params.dwVersion = NVFBC_DESTROY_HANDLE_PARAMS_VER;
+ cap_nvfbc->nv_fbc_function_list.nvFBCDestroyHandle(cap_nvfbc->nv_fbc_handle, &destroy_params);
+ cap_nvfbc->fbc_handle_created = false;
+ cap_nvfbc->nv_fbc_handle = 0;
+ }
+}
+
+static void gsr_capture_nvfbc_destroy_session_and_handle(gsr_capture_nvfbc *cap_nvfbc) {
+ gsr_capture_nvfbc_destroy_session(cap_nvfbc);
+ gsr_capture_nvfbc_destroy_handle(cap_nvfbc);
+}
+
+static int gsr_capture_nvfbc_setup_handle(gsr_capture_nvfbc *cap_nvfbc) {
+ NVFBCSTATUS status;
+
+ NVFBC_CREATE_HANDLE_PARAMS create_params;
+ memset(&create_params, 0, sizeof(create_params));
+ create_params.dwVersion = NVFBC_CREATE_HANDLE_PARAMS_VER;
+ create_params.bExternallyManagedContext = NVFBC_TRUE;
+ create_params.glxCtx = cap_nvfbc->params.egl->glx_context;
+ create_params.glxFBConfig = cap_nvfbc->params.egl->glx_fb_config;
+
+ status = cap_nvfbc->nv_fbc_function_list.nvFBCCreateHandle(&cap_nvfbc->nv_fbc_handle, &create_params);
+ if(status != NVFBC_SUCCESS) {
+ // Reverse engineering for interoperability
+ const uint8_t enable_key[] = { 0xac, 0x10, 0xc9, 0x2e, 0xa5, 0xe6, 0x87, 0x4f, 0x8f, 0x4b, 0xf4, 0x61, 0xf8, 0x56, 0x27, 0xe9 };
+ create_params.privateData = enable_key;
+ create_params.privateDataSize = 16;
+
+ status = cap_nvfbc->nv_fbc_function_list.nvFBCCreateHandle(&cap_nvfbc->nv_fbc_handle, &create_params);
+ if(status != NVFBC_SUCCESS) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", cap_nvfbc->nv_fbc_function_list.nvFBCGetLastErrorStr(cap_nvfbc->nv_fbc_handle));
+ goto error_cleanup;
+ }
+ }
+ cap_nvfbc->fbc_handle_created = true;
+
+ NVFBC_GET_STATUS_PARAMS status_params;
+ memset(&status_params, 0, sizeof(status_params));
+ status_params.dwVersion = NVFBC_GET_STATUS_PARAMS_VER;
+
+ status = cap_nvfbc->nv_fbc_function_list.nvFBCGetStatus(cap_nvfbc->nv_fbc_handle, &status_params);
+ if(status != NVFBC_SUCCESS) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", cap_nvfbc->nv_fbc_function_list.nvFBCGetLastErrorStr(cap_nvfbc->nv_fbc_handle));
+ goto error_cleanup;
+ }
+
+ if(status_params.bCanCreateNow == NVFBC_FALSE) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: it's not possible to create a capture session on this system\n");
+ goto error_cleanup;
+ }
+
+ cap_nvfbc->tracking_width = XWidthOfScreen(DefaultScreenOfDisplay(cap_nvfbc->params.egl->x11.dpy));
+ cap_nvfbc->tracking_height = XHeightOfScreen(DefaultScreenOfDisplay(cap_nvfbc->params.egl->x11.dpy));
+ cap_nvfbc->tracking_type = strcmp(cap_nvfbc->params.display_to_capture, "screen") == 0 ? NVFBC_TRACKING_SCREEN : NVFBC_TRACKING_OUTPUT;
+ if(cap_nvfbc->tracking_type == NVFBC_TRACKING_OUTPUT) {
+ if(!status_params.bXRandRAvailable) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: the xrandr extension is not available\n");
+ goto error_cleanup;
+ }
+
+ if(status_params.bInModeset) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: the x server is in modeset, unable to record\n");
+ goto error_cleanup;
+ }
+
+ cap_nvfbc->output_id = get_output_id_from_display_name(status_params.outputs, status_params.dwOutputNum, cap_nvfbc->params.display_to_capture, &cap_nvfbc->tracking_width, &cap_nvfbc->tracking_height);
+ if(cap_nvfbc->output_id == 0) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: display '%s' not found\n", cap_nvfbc->params.display_to_capture);
+ goto error_cleanup;
+ }
+ }
+
+ return 0;
+
+ error_cleanup:
+ gsr_capture_nvfbc_destroy_session_and_handle(cap_nvfbc);
+ return -1;
+}
+
+static int gsr_capture_nvfbc_setup_session(gsr_capture_nvfbc *cap_nvfbc) {
+ NVFBC_CREATE_CAPTURE_SESSION_PARAMS create_capture_params;
+ memset(&create_capture_params, 0, sizeof(create_capture_params));
+ create_capture_params.dwVersion = NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER;
+ create_capture_params.eCaptureType = NVFBC_CAPTURE_TO_GL;
+ create_capture_params.bWithCursor = (!cap_nvfbc->direct_capture || cap_nvfbc->supports_direct_cursor) ? NVFBC_TRUE : NVFBC_FALSE;
+ if(!cap_nvfbc->params.record_cursor)
+ create_capture_params.bWithCursor = false;
+ if(cap_nvfbc->capture_region)
+ create_capture_params.captureBox = (NVFBC_BOX){ cap_nvfbc->x, cap_nvfbc->y, cap_nvfbc->width, cap_nvfbc->height };
+ create_capture_params.eTrackingType = cap_nvfbc->tracking_type;
+ create_capture_params.dwSamplingRateMs = (uint32_t)ceilf(1000.0f / (float)cap_nvfbc->params.fps);
+ create_capture_params.bAllowDirectCapture = cap_nvfbc->direct_capture ? NVFBC_TRUE : NVFBC_FALSE;
+ create_capture_params.bPushModel = cap_nvfbc->direct_capture ? NVFBC_TRUE : NVFBC_FALSE;
+ create_capture_params.bDisableAutoModesetRecovery = true;
+ if(cap_nvfbc->tracking_type == NVFBC_TRACKING_OUTPUT)
+ create_capture_params.dwOutputId = cap_nvfbc->output_id;
+
+ NVFBCSTATUS status = cap_nvfbc->nv_fbc_function_list.nvFBCCreateCaptureSession(cap_nvfbc->nv_fbc_handle, &create_capture_params);
+ if(status != NVFBC_SUCCESS) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", cap_nvfbc->nv_fbc_function_list.nvFBCGetLastErrorStr(cap_nvfbc->nv_fbc_handle));
+ return -1;
+ }
+ cap_nvfbc->capture_session_created = true;
+
+ memset(&cap_nvfbc->setup_params, 0, sizeof(cap_nvfbc->setup_params));
+ cap_nvfbc->setup_params.dwVersion = NVFBC_TOGL_SETUP_PARAMS_VER;
+ cap_nvfbc->setup_params.eBufferFormat = NVFBC_BUFFER_FORMAT_BGRA;
+
+ status = cap_nvfbc->nv_fbc_function_list.nvFBCToGLSetUp(cap_nvfbc->nv_fbc_handle, &cap_nvfbc->setup_params);
+ if(status != NVFBC_SUCCESS) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", cap_nvfbc->nv_fbc_function_list.nvFBCGetLastErrorStr(cap_nvfbc->nv_fbc_handle));
+ gsr_capture_nvfbc_destroy_session(cap_nvfbc);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int gsr_capture_nvfbc_start(gsr_capture *cap, AVCodecContext *video_codec_context, AVFrame *frame) {
+ gsr_capture_nvfbc *cap_nvfbc = cap->priv;
+
+ cap_nvfbc->base.video_codec_context = video_codec_context;
+ cap_nvfbc->base.egl = cap_nvfbc->params.egl;
+
+ if(!gsr_cuda_load(&cap_nvfbc->cuda, cap_nvfbc->params.egl->x11.dpy, cap_nvfbc->params.overclock))
+ return -1;
+
+ if(!gsr_capture_nvfbc_load_library(cap)) {
+ gsr_cuda_unload(&cap_nvfbc->cuda);
+ return -1;
+ }
+
+ cap_nvfbc->x = max_int(cap_nvfbc->params.pos.x, 0);
+ cap_nvfbc->y = max_int(cap_nvfbc->params.pos.y, 0);
+ cap_nvfbc->width = max_int(cap_nvfbc->params.size.x, 0);
+ cap_nvfbc->height = max_int(cap_nvfbc->params.size.y, 0);
+
+ cap_nvfbc->capture_region = (cap_nvfbc->x > 0 || cap_nvfbc->y > 0 || cap_nvfbc->width > 0 || cap_nvfbc->height > 0);
+
+ cap_nvfbc->supports_direct_cursor = false;
+ bool direct_capture = cap_nvfbc->params.direct_capture;
+ int driver_major_version = 0;
+ int driver_minor_version = 0;
+ if(direct_capture && get_driver_version(&driver_major_version, &driver_minor_version)) {
+ fprintf(stderr, "Info: detected nvidia version: %d.%d\n", driver_major_version, driver_minor_version);
+
+ // TODO:
+ if(version_at_least(driver_major_version, driver_minor_version, 515, 57) && version_less_than(driver_major_version, driver_minor_version, 520, 56)) {
+ direct_capture = false;
+ fprintf(stderr, "Warning: \"screen-direct\" has temporary been disabled as it causes stuttering with driver versions >= 515.57 and < 520.56. Please update your driver if possible. Capturing \"screen\" instead.\n");
+ }
+
+ // TODO:
+ // Cursor capture disabled because moving the cursor doesn't update capture rate to monitor hz and instead captures at 10-30 hz
+ /*
+ if(direct_capture) {
+ if(version_at_least(driver_major_version, driver_minor_version, 515, 57))
+ supports_direct_cursor = true;
+ else
+ fprintf(stderr, "Info: capturing \"screen-direct\" but driver version appears to be less than 515.57. Disabling capture of cursor. Please update your driver if you want to capture your cursor or record \"screen\" instead.\n");
+ }
+ */
+ }
+
+ if(gsr_capture_nvfbc_setup_handle(cap_nvfbc) != 0) {
+ goto error_cleanup;
+ }
+
+ if(gsr_capture_nvfbc_setup_session(cap_nvfbc) != 0) {
+ goto error_cleanup;
+ }
+
+ if(cap_nvfbc->capture_region) {
+ video_codec_context->width = cap_nvfbc->width & ~1;
+ video_codec_context->height = cap_nvfbc->height & ~1;
+ } else {
+ video_codec_context->width = cap_nvfbc->tracking_width & ~1;
+ video_codec_context->height = cap_nvfbc->tracking_height & ~1;
+ }
+
+ frame->width = video_codec_context->width;
+ frame->height = video_codec_context->height;
+
+ if(!cuda_create_codec_context(cap_nvfbc->cuda.cu_ctx, video_codec_context, video_codec_context->width, video_codec_context->height, false, &cap_nvfbc->cuda_stream))
+ goto error_cleanup;
+
+ gsr_cuda_context cuda_context = {
+ .cuda = &cap_nvfbc->cuda,
+ .cuda_graphics_resources = cap_nvfbc->cuda_graphics_resources,
+ .mapped_arrays = cap_nvfbc->mapped_arrays
+ };
+
+ // TODO: Remove this, it creates shit we dont need
+ if(!gsr_capture_base_setup_cuda_textures(&cap_nvfbc->base, frame, &cuda_context, cap_nvfbc->params.color_range, GSR_SOURCE_COLOR_BGR, cap_nvfbc->params.hdr)) {
+ goto error_cleanup;
+ }
+ /* Disable vsync */
+ set_vertical_sync_enabled(cap_nvfbc->params.egl, 0);
+
+ return 0;
+
+ error_cleanup:
+ gsr_capture_nvfbc_destroy_session_and_handle(cap_nvfbc);
+ gsr_capture_base_stop(&cap_nvfbc->base);
+ gsr_cuda_unload(&cap_nvfbc->cuda);
+ return -1;
+}
+
+static int gsr_capture_nvfbc_capture(gsr_capture *cap, AVFrame *frame) {
+ gsr_capture_nvfbc *cap_nvfbc = cap->priv;
+
+ const double nvfbc_recreate_retry_time_seconds = 1.0;
+ if(cap_nvfbc->nvfbc_needs_recreate) {
+ const double now = clock_get_monotonic_seconds();
+ if(now - cap_nvfbc->nvfbc_dead_start >= nvfbc_recreate_retry_time_seconds) {
+ cap_nvfbc->nvfbc_dead_start = now;
+ gsr_capture_nvfbc_destroy_session_and_handle(cap_nvfbc);
+
+ if(gsr_capture_nvfbc_setup_handle(cap_nvfbc) != 0) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_capture failed to recreate nvfbc handle, trying again in %f second(s)\n", nvfbc_recreate_retry_time_seconds);
+ return -1;
+ }
+
+ if(gsr_capture_nvfbc_setup_session(cap_nvfbc) != 0) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_capture failed to recreate nvfbc session, trying again in %f second(s)\n", nvfbc_recreate_retry_time_seconds);
+ return -1;
+ }
+
+ cap_nvfbc->nvfbc_needs_recreate = false;
+ } else {
+ return 0;
+ }
+ }
+
+ NVFBC_FRAME_GRAB_INFO frame_info;
+ memset(&frame_info, 0, sizeof(frame_info));
+
+ NVFBC_TOGL_GRAB_FRAME_PARAMS grab_params;
+ memset(&grab_params, 0, sizeof(grab_params));
+ grab_params.dwVersion = NVFBC_TOGL_GRAB_FRAME_PARAMS_VER;
+ grab_params.dwFlags = NVFBC_TOGL_GRAB_FLAGS_NOWAIT | NVFBC_TOGL_GRAB_FLAGS_FORCE_REFRESH; // TODO: Remove NVFBC_TOGL_GRAB_FLAGS_FORCE_REFRESH
+ grab_params.pFrameGrabInfo = &frame_info;
+ grab_params.dwTimeoutMs = 0;
+
+ NVFBCSTATUS status = cap_nvfbc->nv_fbc_function_list.nvFBCToGLGrabFrame(cap_nvfbc->nv_fbc_handle, &grab_params);
+ if(status != NVFBC_SUCCESS) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_capture failed: %s (%d), recreating session after %f second(s)\n", cap_nvfbc->nv_fbc_function_list.nvFBCGetLastErrorStr(cap_nvfbc->nv_fbc_handle), status, nvfbc_recreate_retry_time_seconds);
+ cap_nvfbc->nvfbc_needs_recreate = true;
+ cap_nvfbc->nvfbc_dead_start = clock_get_monotonic_seconds();
+ return 0;
+ }
+
+ //cap_nvfbc->params.egl->glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ cap_nvfbc->params.egl->glClear(0);
+
+ gsr_color_conversion_draw(&cap_nvfbc->base.color_conversion, cap_nvfbc->setup_params.dwTextures[grab_params.dwTextureIndex],
+ (vec2i){0, 0}, (vec2i){frame->width, frame->height},
+ (vec2i){0, 0}, (vec2i){frame->width, frame->height},
+ 0.0f, false);
+
+ cap_nvfbc->params.egl->glXSwapBuffers(cap_nvfbc->params.egl->x11.dpy, cap_nvfbc->params.egl->x11.window);
+
+ // TODO: HDR is broken
+ const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size
+ for(int i = 0; i < 2; ++i) {
+ CUDA_MEMCPY2D memcpy_struct;
+ memcpy_struct.srcXInBytes = 0;
+ memcpy_struct.srcY = 0;
+ memcpy_struct.srcMemoryType = CU_MEMORYTYPE_ARRAY;
+
+ memcpy_struct.dstXInBytes = 0;
+ memcpy_struct.dstY = 0;
+ memcpy_struct.dstMemoryType = CU_MEMORYTYPE_DEVICE;
+
+ memcpy_struct.srcArray = cap_nvfbc->mapped_arrays[i];
+ memcpy_struct.srcPitch = frame->width / div[i];
+ memcpy_struct.dstDevice = (CUdeviceptr)frame->data[i];
+ memcpy_struct.dstPitch = frame->linesize[i];
+ memcpy_struct.WidthInBytes = frame->width * (cap_nvfbc->params.hdr ? 2 : 1);
+ memcpy_struct.Height = frame->height / div[i];
+ // TODO: Remove this copy if possible
+ cap_nvfbc->cuda.cuMemcpy2DAsync_v2(&memcpy_struct, cap_nvfbc->cuda_stream);
+ }
+
+ // TODO: needed?
+ cap_nvfbc->cuda.cuStreamSynchronize(cap_nvfbc->cuda_stream);
+
+ return 0;
+}
+
+static void gsr_capture_nvfbc_destroy(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ gsr_capture_nvfbc *cap_nvfbc = cap->priv;
+ gsr_capture_nvfbc_destroy_session_and_handle(cap_nvfbc);
+ if(cap_nvfbc) {
+ gsr_capture_base_stop(&cap_nvfbc->base);
+ gsr_cuda_unload(&cap_nvfbc->cuda);
+ dlclose(cap_nvfbc->library);
+ free((void*)cap_nvfbc->params.display_to_capture);
+ cap_nvfbc->params.display_to_capture = NULL;
+ free(cap->priv);
+ cap->priv = NULL;
+ }
+ free(cap);
+}
+
+gsr_capture* gsr_capture_nvfbc_create(const gsr_capture_nvfbc_params *params) {
+ if(!params) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_create params is NULL\n");
+ return NULL;
+ }
+
+ if(!params->display_to_capture) {
+ fprintf(stderr, "gsr error: gsr_capture_nvfbc_create params.display_to_capture is NULL\n");
+ return NULL;
+ }
+
+ gsr_capture *cap = calloc(1, sizeof(gsr_capture));
+ if(!cap)
+ return NULL;
+
+ gsr_capture_nvfbc *cap_nvfbc = calloc(1, sizeof(gsr_capture_nvfbc));
+ if(!cap_nvfbc) {
+ free(cap);
+ return NULL;
+ }
+
+ const char *display_to_capture = strdup(params->display_to_capture);
+ if(!display_to_capture) {
+ free(cap);
+ free(cap_nvfbc);
+ return NULL;
+ }
+
+ cap_nvfbc->params = *params;
+ cap_nvfbc->params.display_to_capture = display_to_capture;
+ cap_nvfbc->params.fps = max_int(cap_nvfbc->params.fps, 1);
+
+ *cap = (gsr_capture) {
+ .start = gsr_capture_nvfbc_start,
+ .tick = NULL,
+ .should_stop = NULL,
+ .capture = gsr_capture_nvfbc_capture,
+ .capture_end = NULL,
+ .destroy = gsr_capture_nvfbc_destroy,
+ .priv = cap_nvfbc
+ };
+
+ return cap;
+}
diff --git a/src/capture/xcomposite.c b/src/capture/xcomposite.c
new file mode 100644
index 0000000..29b42d5
--- /dev/null
+++ b/src/capture/xcomposite.c
@@ -0,0 +1,299 @@
+#include "../../include/capture/xcomposite.h"
+#include "../../include/window_texture.h"
+#include "../../include/utils.h"
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <assert.h>
+#include <X11/Xlib.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/hwcontext.h>
+#include <libavutil/frame.h>
+#include <libavcodec/avcodec.h>
+#include <va/va.h>
+#include <va/va_drmcommon.h>
+
+static int max_int(int a, int b) {
+ return a > b ? a : b;
+}
+
+static int min_int(int a, int b) {
+ return a < b ? a : b;
+}
+
+void gsr_capture_xcomposite_init(gsr_capture_xcomposite *self, const gsr_capture_xcomposite_params *params) {
+ memset(self, 0, sizeof(*self));
+ self->params = *params;
+}
+
+static Window get_focused_window(Display *display, Atom net_active_window_atom) {
+ Atom type;
+ int format = 0;
+ unsigned long num_items = 0;
+ unsigned long bytes_after = 0;
+ unsigned char *properties = NULL;
+ if(XGetWindowProperty(display, DefaultRootWindow(display), net_active_window_atom, 0, 1024, False, AnyPropertyType, &type, &format, &num_items, &bytes_after, &properties) == Success && properties) {
+ Window focused_window = *(unsigned long*)properties;
+ XFree(properties);
+ return focused_window;
+ }
+ return None;
+}
+
+int gsr_capture_xcomposite_start(gsr_capture_xcomposite *self, AVCodecContext *video_codec_context, AVFrame *frame) {
+ self->base.video_codec_context = video_codec_context;
+ self->base.egl = self->params.egl;
+
+ if(self->params.follow_focused) {
+ self->net_active_window_atom = XInternAtom(self->params.egl->x11.dpy, "_NET_ACTIVE_WINDOW", False);
+ if(!self->net_active_window_atom) {
+ fprintf(stderr, "gsr error: gsr_capture_xcomposite_start failed: failed to get _NET_ACTIVE_WINDOW atom\n");
+ return -1;
+ }
+ self->window = get_focused_window(self->params.egl->x11.dpy, self->net_active_window_atom);
+ } else {
+ self->window = self->params.window;
+ }
+
+ /* TODO: Do these in tick, and allow error if follow_focused */
+
+ XWindowAttributes attr;
+ if(!XGetWindowAttributes(self->params.egl->x11.dpy, self->window, &attr) && !self->params.follow_focused) {
+ fprintf(stderr, "gsr error: gsr_capture_xcomposite_start failed: invalid window id: %lu\n", self->window);
+ return -1;
+ }
+
+ self->window_size.x = max_int(attr.width, 0);
+ self->window_size.y = max_int(attr.height, 0);
+
+ if(self->params.follow_focused)
+ XSelectInput(self->params.egl->x11.dpy, DefaultRootWindow(self->params.egl->x11.dpy), PropertyChangeMask);
+
+ // TODO: Get select and add these on top of it and then restore at the end. Also do the same in other xcomposite
+ XSelectInput(self->params.egl->x11.dpy, self->window, StructureNotifyMask | ExposureMask);
+
+ if(!self->params.egl->eglExportDMABUFImageQueryMESA) {
+ fprintf(stderr, "gsr error: gsr_capture_xcomposite_start: could not find eglExportDMABUFImageQueryMESA\n");
+ return -1;
+ }
+
+ if(!self->params.egl->eglExportDMABUFImageMESA) {
+ fprintf(stderr, "gsr error: gsr_capture_xcomposite_start: could not find eglExportDMABUFImageMESA\n");
+ return -1;
+ }
+
+ /* Disable vsync */
+ self->params.egl->eglSwapInterval(self->params.egl->egl_display, 0);
+ if(window_texture_init(&self->window_texture, self->params.egl->x11.dpy, self->window, self->params.egl) != 0 && !self->params.follow_focused) {
+ fprintf(stderr, "gsr error: gsr_capture_xcomposite_start: failed to get window texture for window %ld\n", self->window);
+ return -1;
+ }
+
+ if(gsr_cursor_init(&self->cursor, self->params.egl, self->params.egl->x11.dpy) != 0) {
+ gsr_capture_xcomposite_stop(self);
+ return -1;
+ }
+
+ self->texture_size.x = 0;
+ self->texture_size.y = 0;
+
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, window_texture_get_opengl_texture_id(&self->window_texture));
+ self->params.egl->glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &self->texture_size.x);
+ self->params.egl->glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &self->texture_size.y);
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ self->texture_size.x = max_int(2, even_number_ceil(self->texture_size.x));
+ self->texture_size.y = max_int(2, even_number_ceil(self->texture_size.y));
+
+ video_codec_context->width = self->texture_size.x;
+ video_codec_context->height = self->texture_size.y;
+
+ if(self->params.region_size.x > 0 && self->params.region_size.y > 0) {
+ video_codec_context->width = max_int(2, even_number_ceil(self->params.region_size.x));
+ video_codec_context->height = max_int(2, even_number_ceil(self->params.region_size.y));
+ }
+
+ frame->width = video_codec_context->width;
+ frame->height = video_codec_context->height;
+
+ self->window_resize_timer = clock_get_monotonic_seconds();
+ self->clear_next_frame = true;
+ return 0;
+}
+
+void gsr_capture_xcomposite_stop(gsr_capture_xcomposite *self) {
+ window_texture_deinit(&self->window_texture);
+ gsr_cursor_deinit(&self->cursor);
+ gsr_capture_base_stop(&self->base);
+}
+
+void gsr_capture_xcomposite_tick(gsr_capture_xcomposite *self, AVCodecContext *video_codec_context) {
+ //self->params.egl->glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+ self->params.egl->glClear(0);
+
+ bool init_new_window = false;
+ while(XPending(self->params.egl->x11.dpy)) {
+ XNextEvent(self->params.egl->x11.dpy, &self->xev);
+
+ switch(self->xev.type) {
+ case DestroyNotify: {
+ /* Window died (when not following focused window), so we stop recording */
+ if(!self->params.follow_focused && self->xev.xdestroywindow.window == self->window) {
+ self->should_stop = true;
+ self->stop_is_error = false;
+ }
+ break;
+ }
+ case Expose: {
+ /* Requires window texture recreate */
+ if(self->xev.xexpose.count == 0 && self->xev.xexpose.window == self->window) {
+ self->window_resize_timer = clock_get_monotonic_seconds();
+ self->window_resized = true;
+ }
+ break;
+ }
+ case ConfigureNotify: {
+ /* Window resized */
+ if(self->xev.xconfigure.window == self->window && (self->xev.xconfigure.width != self->window_size.x || self->xev.xconfigure.height != self->window_size.y)) {
+ self->window_size.x = max_int(self->xev.xconfigure.width, 0);
+ self->window_size.y = max_int(self->xev.xconfigure.height, 0);
+ self->window_resize_timer = clock_get_monotonic_seconds();
+ self->window_resized = true;
+ }
+ break;
+ }
+ case PropertyNotify: {
+ /* Focused window changed */
+ if(self->params.follow_focused && self->xev.xproperty.atom == self->net_active_window_atom) {
+ init_new_window = true;
+ }
+ break;
+ }
+ }
+
+ gsr_cursor_update(&self->cursor, &self->xev);
+ }
+
+ if(self->params.follow_focused && !self->follow_focused_initialized) {
+ init_new_window = true;
+ }
+
+ if(init_new_window) {
+ Window focused_window = get_focused_window(self->params.egl->x11.dpy, self->net_active_window_atom);
+ if(focused_window != self->window || !self->follow_focused_initialized) {
+ self->follow_focused_initialized = true;
+ XSelectInput(self->params.egl->x11.dpy, self->window, 0);
+ self->window = focused_window;
+ XSelectInput(self->params.egl->x11.dpy, self->window, StructureNotifyMask | ExposureMask);
+
+ XWindowAttributes attr;
+ attr.width = 0;
+ attr.height = 0;
+ if(!XGetWindowAttributes(self->params.egl->x11.dpy, self->window, &attr))
+ fprintf(stderr, "gsr error: gsr_capture_xcomposite_tick failed: invalid window id: %lu\n", self->window);
+
+ self->window_size.x = max_int(attr.width, 0);
+ self->window_size.y = max_int(attr.height, 0);
+ self->window_resized = true;
+
+ window_texture_deinit(&self->window_texture);
+ window_texture_init(&self->window_texture, self->params.egl->x11.dpy, self->window, self->params.egl); // TODO: Do not do the below window_texture_on_resize after this
+ }
+ }
+
+ const double window_resize_timeout = 1.0; // 1 second
+ if(self->window_resized && clock_get_monotonic_seconds() - self->window_resize_timer >= window_resize_timeout) {
+ self->window_resized = false;
+
+ if(window_texture_on_resize(&self->window_texture) != 0) {
+ fprintf(stderr, "gsr error: gsr_capture_xcomposite_tick: window_texture_on_resize failed\n");
+ //self->should_stop = true;
+ //self->stop_is_error = true;
+ return;
+ }
+
+ self->texture_size.x = 0;
+ self->texture_size.y = 0;
+
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, window_texture_get_opengl_texture_id(&self->window_texture));
+ self->params.egl->glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &self->texture_size.x);
+ self->params.egl->glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &self->texture_size.y);
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ self->texture_size.x = min_int(video_codec_context->width, max_int(2, even_number_ceil(self->texture_size.x)));
+ self->texture_size.y = min_int(video_codec_context->height, max_int(2, even_number_ceil(self->texture_size.y)));
+
+ gsr_color_conversion_clear(&self->base.color_conversion);
+ }
+}
+
+bool gsr_capture_xcomposite_should_stop(gsr_capture_xcomposite *self, bool *err) {
+ if(self->should_stop) {
+ if(err)
+ *err = self->stop_is_error;
+ return true;
+ }
+
+ if(err)
+ *err = false;
+ return false;
+}
+
+int gsr_capture_xcomposite_capture(gsr_capture_xcomposite *self, AVFrame *frame) {
+ (void)frame;
+
+ const int target_x = max_int(0, frame->width / 2 - self->texture_size.x / 2);
+ const int target_y = max_int(0, frame->height / 2 - self->texture_size.y / 2);
+
+ // TODO: Can we do this a better way than to call it every capture?
+ if(self->params.record_cursor)
+ gsr_cursor_tick(&self->cursor, self->window);
+
+ const vec2i cursor_pos = {
+ target_x + self->cursor.position.x - self->cursor.hotspot.x,
+ target_y + self->cursor.position.y - self->cursor.hotspot.y
+ };
+
+ const bool cursor_completely_inside_window =
+ cursor_pos.x >= target_x &&
+ cursor_pos.x + self->cursor.size.x <= target_x + self->texture_size.x &&
+ cursor_pos.y >= target_y &&
+ cursor_pos.y + self->cursor.size.y <= target_y + self->texture_size.y;
+
+ const bool cursor_inside_window =
+ cursor_pos.x + self->cursor.size.x >= target_x &&
+ cursor_pos.x <= target_x + self->texture_size.x &&
+ cursor_pos.y + self->cursor.size.y >= target_y &&
+ cursor_pos.y <= target_y + self->texture_size.y;
+
+ if(self->clear_next_frame) {
+ self->clear_next_frame = false;
+ gsr_color_conversion_clear(&self->base.color_conversion);
+ }
+
+ /*
+ We dont draw the cursor if it's outside the window but if it's partially inside the window then the cursor area that is outside the window
+ will not get overdrawn the next frame causing a cursor trail to be visible since we dont clear the background.
+ To fix this we detect if the cursor is partially inside the window and clear the background only in that case.
+ */
+ if(!cursor_completely_inside_window && cursor_inside_window && self->params.record_cursor)
+ self->clear_next_frame = true;
+
+ gsr_color_conversion_draw(&self->base.color_conversion, window_texture_get_opengl_texture_id(&self->window_texture),
+ (vec2i){target_x, target_y}, self->texture_size,
+ (vec2i){0, 0}, self->texture_size,
+ 0.0f, false);
+
+ if(cursor_inside_window && self->params.record_cursor) {
+ gsr_color_conversion_draw(&self->base.color_conversion, self->cursor.texture_id,
+ cursor_pos, self->cursor.size,
+ (vec2i){0, 0}, self->cursor.size,
+ 0.0f, false);
+ }
+
+ self->params.egl->eglSwapBuffers(self->params.egl->egl_display, self->params.egl->egl_surface);
+ //self->params.egl->glFlush();
+ //self->params.egl->glFinish();
+
+ return 0;
+}
diff --git a/src/capture/xcomposite_cuda.c b/src/capture/xcomposite_cuda.c
new file mode 100644
index 0000000..6e13d2a
--- /dev/null
+++ b/src/capture/xcomposite_cuda.c
@@ -0,0 +1,155 @@
+#include "../../include/capture/xcomposite_cuda.h"
+#include "../../include/cuda.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <libavutil/frame.h>
+#include <libavcodec/avcodec.h>
+
+typedef struct {
+ gsr_capture_xcomposite xcomposite;
+ bool overclock;
+
+ gsr_cuda cuda;
+ CUgraphicsResource cuda_graphics_resources[2];
+ CUarray mapped_arrays[2];
+ CUstream cuda_stream;
+} gsr_capture_xcomposite_cuda;
+
+static void gsr_capture_xcomposite_cuda_stop(gsr_capture *cap, AVCodecContext *video_codec_context);
+
+static int gsr_capture_xcomposite_cuda_start(gsr_capture *cap, AVCodecContext *video_codec_context, AVFrame *frame) {
+ gsr_capture_xcomposite_cuda *cap_xcomp = cap->priv;
+
+ const int res = gsr_capture_xcomposite_start(&cap_xcomp->xcomposite, video_codec_context, frame);
+ if(res != 0) {
+ gsr_capture_xcomposite_cuda_stop(cap, video_codec_context);
+ return res;
+ }
+
+ if(!gsr_cuda_load(&cap_xcomp->cuda, cap_xcomp->xcomposite.params.egl->x11.dpy, cap_xcomp->overclock)) {
+ fprintf(stderr, "gsr error: gsr_capture_kms_cuda_start: failed to load cuda\n");
+ gsr_capture_xcomposite_cuda_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ if(!cuda_create_codec_context(cap_xcomp->cuda.cu_ctx, video_codec_context, video_codec_context->width, video_codec_context->height, false, &cap_xcomp->cuda_stream)) {
+ gsr_capture_xcomposite_cuda_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ gsr_cuda_context cuda_context = {
+ .cuda = &cap_xcomp->cuda,
+ .cuda_graphics_resources = cap_xcomp->cuda_graphics_resources,
+ .mapped_arrays = cap_xcomp->mapped_arrays
+ };
+
+ if(!gsr_capture_base_setup_cuda_textures(&cap_xcomp->xcomposite.base, frame, &cuda_context, cap_xcomp->xcomposite.params.color_range, GSR_SOURCE_COLOR_RGB, false)) {
+ gsr_capture_xcomposite_cuda_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void gsr_capture_xcomposite_unload_cuda_graphics(gsr_capture_xcomposite_cuda *cap_xcomp) {
+ if(cap_xcomp->cuda.cu_ctx) {
+ for(int i = 0; i < 2; ++i) {
+ if(cap_xcomp->cuda_graphics_resources[i]) {
+ cap_xcomp->cuda.cuGraphicsUnmapResources(1, &cap_xcomp->cuda_graphics_resources[i], 0);
+ cap_xcomp->cuda.cuGraphicsUnregisterResource(cap_xcomp->cuda_graphics_resources[i]);
+ cap_xcomp->cuda_graphics_resources[i] = 0;
+ }
+ }
+ }
+}
+
+static void gsr_capture_xcomposite_cuda_stop(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ gsr_capture_xcomposite_cuda *cap_xcomp = cap->priv;
+ gsr_capture_xcomposite_stop(&cap_xcomp->xcomposite);
+ gsr_capture_xcomposite_unload_cuda_graphics(cap_xcomp);
+ gsr_cuda_unload(&cap_xcomp->cuda);
+}
+
+static void gsr_capture_xcomposite_cuda_tick(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ gsr_capture_xcomposite_cuda *cap_xcomp = cap->priv;
+ gsr_capture_xcomposite_tick(&cap_xcomp->xcomposite, video_codec_context);
+}
+
+static bool gsr_capture_xcomposite_cuda_should_stop(gsr_capture *cap, bool *err) {
+ gsr_capture_xcomposite_cuda *cap_xcomp = cap->priv;
+ return gsr_capture_xcomposite_should_stop(&cap_xcomp->xcomposite, err);
+}
+
+static int gsr_capture_xcomposite_cuda_capture(gsr_capture *cap, AVFrame *frame) {
+ gsr_capture_xcomposite_cuda *cap_xcomp = cap->priv;
+
+ gsr_capture_xcomposite_capture(&cap_xcomp->xcomposite, frame);
+
+ const int div[2] = {1, 2}; // divide UV texture size by 2 because chroma is half size
+ for(int i = 0; i < 2; ++i) {
+ CUDA_MEMCPY2D memcpy_struct;
+ memcpy_struct.srcXInBytes = 0;
+ memcpy_struct.srcY = 0;
+ memcpy_struct.srcMemoryType = CU_MEMORYTYPE_ARRAY;
+
+ memcpy_struct.dstXInBytes = 0;
+ memcpy_struct.dstY = 0;
+ memcpy_struct.dstMemoryType = CU_MEMORYTYPE_DEVICE;
+
+ memcpy_struct.srcArray = cap_xcomp->mapped_arrays[i];
+ memcpy_struct.srcPitch = frame->width / div[i];
+ memcpy_struct.dstDevice = (CUdeviceptr)frame->data[i];
+ memcpy_struct.dstPitch = frame->linesize[i];
+ memcpy_struct.WidthInBytes = frame->width;
+ memcpy_struct.Height = frame->height / div[i];
+ // TODO: Remove this copy if possible
+ cap_xcomp->cuda.cuMemcpy2DAsync_v2(&memcpy_struct, cap_xcomp->cuda_stream);
+ }
+
+ // TODO: needed?
+ cap_xcomp->cuda.cuStreamSynchronize(cap_xcomp->cuda_stream);
+
+ return 0;
+}
+
+static void gsr_capture_xcomposite_cuda_destroy(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ if(cap->priv) {
+ gsr_capture_xcomposite_cuda_stop(cap, video_codec_context);
+ free(cap->priv);
+ cap->priv = NULL;
+ }
+ free(cap);
+}
+
+gsr_capture* gsr_capture_xcomposite_cuda_create(const gsr_capture_xcomposite_cuda_params *params) {
+ if(!params) {
+ fprintf(stderr, "gsr error: gsr_capture_xcomposite_cuda_create params is NULL\n");
+ return NULL;
+ }
+
+ gsr_capture *cap = calloc(1, sizeof(gsr_capture));
+ if(!cap)
+ return NULL;
+
+ gsr_capture_xcomposite_cuda *cap_xcomp = calloc(1, sizeof(gsr_capture_xcomposite_cuda));
+ if(!cap_xcomp) {
+ free(cap);
+ return NULL;
+ }
+
+ gsr_capture_xcomposite_init(&cap_xcomp->xcomposite, &params->base);
+ cap_xcomp->overclock = params->overclock;
+
+ *cap = (gsr_capture) {
+ .start = gsr_capture_xcomposite_cuda_start,
+ .tick = gsr_capture_xcomposite_cuda_tick,
+ .should_stop = gsr_capture_xcomposite_cuda_should_stop,
+ .capture = gsr_capture_xcomposite_cuda_capture,
+ .capture_end = NULL,
+ .destroy = gsr_capture_xcomposite_cuda_destroy,
+ .priv = cap_xcomp
+ };
+
+ return cap;
+}
diff --git a/src/capture/xcomposite_vaapi.c b/src/capture/xcomposite_vaapi.c
new file mode 100644
index 0000000..8c9c56c
--- /dev/null
+++ b/src/capture/xcomposite_vaapi.c
@@ -0,0 +1,109 @@
+#include "../../include/capture/xcomposite_vaapi.h"
+#include "../../include/capture/xcomposite.h"
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <va/va.h>
+#include <va/va_drmcommon.h>
+#include <libavcodec/avcodec.h>
+
+typedef struct {
+ gsr_capture_xcomposite xcomposite;
+
+ VADisplay va_dpy;
+ VADRMPRIMESurfaceDescriptor prime;
+} gsr_capture_xcomposite_vaapi;
+
+static void gsr_capture_xcomposite_vaapi_stop(gsr_capture *cap, AVCodecContext *video_codec_context);
+
+static int gsr_capture_xcomposite_vaapi_start(gsr_capture *cap, AVCodecContext *video_codec_context, AVFrame *frame) {
+ gsr_capture_xcomposite_vaapi *cap_xcomp = cap->priv;
+
+ const int res = gsr_capture_xcomposite_start(&cap_xcomp->xcomposite, video_codec_context, frame);
+ if(res != 0) {
+ gsr_capture_xcomposite_vaapi_stop(cap, video_codec_context);
+ return res;
+ }
+
+ if(!drm_create_codec_context(cap_xcomp->xcomposite.params.egl->card_path, video_codec_context, video_codec_context->width, video_codec_context->height, false, &cap_xcomp->va_dpy)) {
+ gsr_capture_xcomposite_vaapi_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ if(!gsr_capture_base_setup_vaapi_textures(&cap_xcomp->xcomposite.base, frame, cap_xcomp->va_dpy, &cap_xcomp->prime, cap_xcomp->xcomposite.params.color_range)) {
+ gsr_capture_xcomposite_vaapi_stop(cap, video_codec_context);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void gsr_capture_xcomposite_vaapi_tick(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ gsr_capture_xcomposite_vaapi *cap_xcomp = cap->priv;
+ gsr_capture_xcomposite_tick(&cap_xcomp->xcomposite, video_codec_context);
+}
+
+static bool gsr_capture_xcomposite_vaapi_should_stop(gsr_capture *cap, bool *err) {
+ gsr_capture_xcomposite_vaapi *cap_xcomp = cap->priv;
+ return gsr_capture_xcomposite_should_stop(&cap_xcomp->xcomposite, err);
+}
+
+static int gsr_capture_xcomposite_vaapi_capture(gsr_capture *cap, AVFrame *frame) {
+ gsr_capture_xcomposite_vaapi *cap_xcomp = cap->priv;
+ return gsr_capture_xcomposite_capture(&cap_xcomp->xcomposite, frame);
+}
+
+static void gsr_capture_xcomposite_vaapi_stop(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ gsr_capture_xcomposite_vaapi *cap_xcomp = cap->priv;
+
+ for(uint32_t i = 0; i < cap_xcomp->prime.num_objects; ++i) {
+ if(cap_xcomp->prime.objects[i].fd > 0) {
+ close(cap_xcomp->prime.objects[i].fd);
+ cap_xcomp->prime.objects[i].fd = 0;
+ }
+ }
+
+ gsr_capture_xcomposite_stop(&cap_xcomp->xcomposite);
+}
+
+static void gsr_capture_xcomposite_vaapi_destroy(gsr_capture *cap, AVCodecContext *video_codec_context) {
+ (void)video_codec_context;
+ if(cap->priv) {
+ gsr_capture_xcomposite_vaapi_stop(cap, video_codec_context);
+ free(cap->priv);
+ cap->priv = NULL;
+ }
+ free(cap);
+}
+
+gsr_capture* gsr_capture_xcomposite_vaapi_create(const gsr_capture_xcomposite_vaapi_params *params) {
+ if(!params) {
+ fprintf(stderr, "gsr error: gsr_capture_xcomposite_vaapi_create params is NULL\n");
+ return NULL;
+ }
+
+ gsr_capture *cap = calloc(1, sizeof(gsr_capture));
+ if(!cap)
+ return NULL;
+
+ gsr_capture_xcomposite_vaapi *cap_xcomp = calloc(1, sizeof(gsr_capture_xcomposite_vaapi));
+ if(!cap_xcomp) {
+ free(cap);
+ return NULL;
+ }
+
+ gsr_capture_xcomposite_init(&cap_xcomp->xcomposite, &params->base);
+
+ *cap = (gsr_capture) {
+ .start = gsr_capture_xcomposite_vaapi_start,
+ .tick = gsr_capture_xcomposite_vaapi_tick,
+ .should_stop = gsr_capture_xcomposite_vaapi_should_stop,
+ .capture = gsr_capture_xcomposite_vaapi_capture,
+ .capture_end = NULL,
+ .destroy = gsr_capture_xcomposite_vaapi_destroy,
+ .priv = cap_xcomp
+ };
+
+ return cap;
+}
diff --git a/src/color_conversion.c b/src/color_conversion.c
new file mode 100644
index 0000000..cd0397e
--- /dev/null
+++ b/src/color_conversion.c
@@ -0,0 +1,469 @@
+#include "../include/color_conversion.h"
+#include "../include/egl.h"
+#include <stdio.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+
+/* TODO: highp instead of mediump? */
+
+#define MAX_SHADERS 4
+#define MAX_FRAMEBUFFERS 2
+
+static float abs_f(float v) {
+ return v >= 0.0f ? v : -v;
+}
+
+#define ROTATE_Z "mat4 rotate_z(in float angle) {\n" \
+ " return mat4(cos(angle), -sin(angle), 0.0, 0.0,\n" \
+ " sin(angle), cos(angle), 0.0, 0.0,\n" \
+ " 0.0, 0.0, 1.0, 0.0,\n" \
+ " 0.0, 0.0, 0.0, 1.0);\n" \
+ "}\n"
+
+/* https://en.wikipedia.org/wiki/YCbCr, see study/color_space_transform_matrix.png */
+
+/* ITU-R BT2020, full */
+/* https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2020-2-201510-I!!PDF-E.pdf */
+#define RGB_TO_P010_FULL "const mat4 RGBtoYUV = mat4(0.262700, -0.139630, 0.500000, 0.000000,\n" \
+ " 0.678000, -0.360370, -0.459786, 0.000000,\n" \
+ " 0.059300, 0.500000, -0.040214, 0.000000,\n" \
+ " 0.000000, 0.500000, 0.500000, 1.000000);"
+
+/* ITU-R BT2020, limited (full multiplied by (235-16)/255, adding 16/255 to luma) */
+#define RGB_TO_P010_LIMITED "const mat4 RGBtoYUV = mat4(0.225613, -0.119918, 0.429412, 0.000000,\n" \
+ " 0.582282, -0.309494, -0.394875, 0.000000,\n" \
+ " 0.050928, 0.429412, -0.034537, 0.000000,\n" \
+ " 0.062745, 0.500000, 0.500000, 1.000000);"
+
+/* ITU-R BT709, full, custom values: 0.2110 0.7110 0.0710 */
+/* https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf */
+#define RGB_TO_NV12_FULL "const mat4 RGBtoYUV = mat4(0.211000, -0.113563, 0.500000, 0.000000,\n" \
+ " 0.711000, -0.382670, -0.450570, 0.000000,\n" \
+ " 0.071000, 0.500000, -0.044994, 0.000000,\n" \
+ " 0.000000, 0.500000, 0.500000, 1.000000);"
+
+/* ITU-R BT709, limited, custom values: 0.2100 0.7100 0.0700 (full multiplied by (235-16)/255, adding 16/255 to luma) */
+#define RGB_TO_NV12_LIMITED "const mat4 RGBtoYUV = mat4(0.180353, -0.096964, 0.429412, 0.000000,\n" \
+ " 0.609765, -0.327830, -0.385927, 0.000000,\n" \
+ " 0.060118, 0.429412, -0.038049, 0.000000,\n" \
+ " 0.062745, 0.500000, 0.500000, 1.000000);"
+
+static const char* color_format_range_get_transform_matrix(gsr_destination_color color_format, gsr_color_range color_range) {
+ switch(color_format) {
+ case GSR_DESTINATION_COLOR_NV12: {
+ switch(color_range) {
+ case GSR_COLOR_RANGE_LIMITED:
+ return RGB_TO_NV12_LIMITED;
+ case GSR_COLOR_RANGE_FULL:
+ return RGB_TO_NV12_FULL;
+ }
+ break;
+ }
+ case GSR_DESTINATION_COLOR_P010: {
+ switch(color_range) {
+ case GSR_COLOR_RANGE_LIMITED:
+ return RGB_TO_P010_LIMITED;
+ case GSR_COLOR_RANGE_FULL:
+ return RGB_TO_P010_FULL;
+ }
+ break;
+ }
+ default:
+ return NULL;
+ }
+ return NULL;
+}
+
+static int load_shader_y(gsr_shader *shader, gsr_egl *egl, gsr_color_uniforms *uniforms, gsr_destination_color color_format, gsr_color_range color_range, bool external_texture) {
+ const char *color_transform_matrix = color_format_range_get_transform_matrix(color_format, color_range);
+
+ char vertex_shader[2048];
+ snprintf(vertex_shader, sizeof(vertex_shader),
+ "#version 300 es \n"
+ "in vec2 pos; \n"
+ "in vec2 texcoords; \n"
+ "out vec2 texcoords_out; \n"
+ "uniform vec2 offset; \n"
+ "uniform float rotation; \n"
+ ROTATE_Z
+ "void main() \n"
+ "{ \n"
+ " texcoords_out = (vec4(texcoords.x - 0.5, texcoords.y - 0.5, 0.0, 0.0) * rotate_z(rotation)).xy + vec2(0.5, 0.5); \n"
+ " gl_Position = vec4(offset.x, offset.y, 0.0, 0.0) + vec4(pos.x, pos.y, 0.0, 1.0); \n"
+ "} \n");
+
+ char fragment_shader[2048];
+ if(external_texture) {
+ snprintf(fragment_shader, sizeof(fragment_shader),
+ "#version 300 es \n"
+ "#extension GL_OES_EGL_image_external : enable \n"
+ "#extension GL_OES_EGL_image_external_essl3 : require \n"
+ "precision mediump float; \n"
+ "in vec2 texcoords_out; \n"
+ "uniform samplerExternalOES tex1; \n"
+ "out vec4 FragColor; \n"
+ "%s"
+ "void main() \n"
+ "{ \n"
+ " vec4 pixel = texture(tex1, texcoords_out); \n"
+ " FragColor.x = (RGBtoYUV * vec4(pixel.rgb, 1.0)).x; \n"
+ " FragColor.w = pixel.a; \n"
+ "} \n", color_transform_matrix);
+ } else {
+ snprintf(fragment_shader, sizeof(fragment_shader),
+ "#version 300 es \n"
+ "precision mediump float; \n"
+ "in vec2 texcoords_out; \n"
+ "uniform sampler2D tex1; \n"
+ "out vec4 FragColor; \n"
+ "%s"
+ "void main() \n"
+ "{ \n"
+ " vec4 pixel = texture(tex1, texcoords_out); \n"
+ " FragColor.x = (RGBtoYUV * vec4(pixel.rgb, 1.0)).x; \n"
+ " FragColor.w = pixel.a; \n"
+ "} \n", color_transform_matrix);
+ }
+
+ if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0)
+ return -1;
+
+ gsr_shader_bind_attribute_location(shader, "pos", 0);
+ gsr_shader_bind_attribute_location(shader, "texcoords", 1);
+ uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset");
+ uniforms->rotation = egl->glGetUniformLocation(shader->program_id, "rotation");
+ return 0;
+}
+
+static unsigned int load_shader_uv(gsr_shader *shader, gsr_egl *egl, gsr_color_uniforms *uniforms, gsr_destination_color color_format, gsr_color_range color_range, bool external_texture) {
+ const char *color_transform_matrix = color_format_range_get_transform_matrix(color_format, color_range);
+
+ char vertex_shader[2048];
+ snprintf(vertex_shader, sizeof(vertex_shader),
+ "#version 300 es \n"
+ "in vec2 pos; \n"
+ "in vec2 texcoords; \n"
+ "out vec2 texcoords_out; \n"
+ "uniform vec2 offset; \n"
+ "uniform float rotation; \n"
+ ROTATE_Z
+ "void main() \n"
+ "{ \n"
+ " texcoords_out = (vec4(texcoords.x - 0.5, texcoords.y - 0.5, 0.0, 0.0) * rotate_z(rotation)).xy + vec2(0.5, 0.5); \n"
+ " gl_Position = (vec4(offset.x, offset.y, 0.0, 0.0) + vec4(pos.x, pos.y, 0.0, 1.0)) * vec4(0.5, 0.5, 1.0, 1.0) - vec4(0.5, 0.5, 0.0, 0.0); \n"
+ "} \n");
+
+ char fragment_shader[2048];
+ if(external_texture) {
+ snprintf(fragment_shader, sizeof(fragment_shader),
+ "#version 300 es \n"
+ "#extension GL_OES_EGL_image_external : enable \n"
+ "#extension GL_OES_EGL_image_external_essl3 : require \n"
+ "precision mediump float; \n"
+ "in vec2 texcoords_out; \n"
+ "uniform samplerExternalOES tex1; \n"
+ "out vec4 FragColor; \n"
+ "%s"
+ "void main() \n"
+ "{ \n"
+ " vec4 pixel = texture(tex1, texcoords_out); \n"
+ " FragColor.xy = (RGBtoYUV * vec4(pixel.rgb, 1.0)).yz; \n"
+ " FragColor.w = pixel.a; \n"
+ "} \n", color_transform_matrix);
+ } else {
+ snprintf(fragment_shader, sizeof(fragment_shader),
+ "#version 300 es \n"
+ "precision mediump float; \n"
+ "in vec2 texcoords_out; \n"
+ "uniform sampler2D tex1; \n"
+ "out vec4 FragColor; \n"
+ "%s"
+ "void main() \n"
+ "{ \n"
+ " vec4 pixel = texture(tex1, texcoords_out); \n"
+ " FragColor.xy = (RGBtoYUV * vec4(pixel.rgb, 1.0)).yz; \n"
+ " FragColor.w = pixel.a; \n"
+ "} \n", color_transform_matrix);
+ }
+
+ if(gsr_shader_init(shader, egl, vertex_shader, fragment_shader) != 0)
+ return -1;
+
+ gsr_shader_bind_attribute_location(shader, "pos", 0);
+ gsr_shader_bind_attribute_location(shader, "texcoords", 1);
+ uniforms->offset = egl->glGetUniformLocation(shader->program_id, "offset");
+ uniforms->rotation = egl->glGetUniformLocation(shader->program_id, "rotation");
+ return 0;
+}
+
+static int load_framebuffers(gsr_color_conversion *self) {
+ /* TODO: Only generate the necessary amount of framebuffers (self->params.num_destination_textures) */
+ const unsigned int draw_buffer = GL_COLOR_ATTACHMENT0;
+ self->params.egl->glGenFramebuffers(MAX_FRAMEBUFFERS, self->framebuffers);
+
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[0]);
+ self->params.egl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->params.destination_textures[0], 0);
+ self->params.egl->glDrawBuffers(1, &draw_buffer);
+ if(self->params.egl->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to create framebuffer for Y\n");
+ goto err;
+ }
+
+ if(self->params.num_destination_textures > 1) {
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[1]);
+ self->params.egl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->params.destination_textures[1], 0);
+ self->params.egl->glDrawBuffers(1, &draw_buffer);
+ if(self->params.egl->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to create framebuffer for UV\n");
+ goto err;
+ }
+ }
+
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ return 0;
+
+ err:
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ return -1;
+}
+
+static int create_vertices(gsr_color_conversion *self) {
+ self->params.egl->glGenVertexArrays(1, &self->vertex_array_object_id);
+ self->params.egl->glBindVertexArray(self->vertex_array_object_id);
+
+ self->params.egl->glGenBuffers(1, &self->vertex_buffer_object_id);
+ self->params.egl->glBindBuffer(GL_ARRAY_BUFFER, self->vertex_buffer_object_id);
+ self->params.egl->glBufferData(GL_ARRAY_BUFFER, 24 * sizeof(float), NULL, GL_STREAM_DRAW);
+
+ self->params.egl->glEnableVertexAttribArray(0);
+ self->params.egl->glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
+
+ self->params.egl->glEnableVertexAttribArray(1);
+ self->params.egl->glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
+
+ self->params.egl->glBindVertexArray(0);
+ return 0;
+}
+
+int gsr_color_conversion_init(gsr_color_conversion *self, const gsr_color_conversion_params *params) {
+ assert(params);
+ assert(params->egl);
+ memset(self, 0, sizeof(*self));
+ self->params.egl = params->egl;
+ self->params = *params;
+
+ switch(params->destination_color) {
+ case GSR_DESTINATION_COLOR_NV12:
+ case GSR_DESTINATION_COLOR_P010: {
+ if(self->params.num_destination_textures != 2) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_init: expected 2 destination textures for destination color NV12/P010, got %d destination texture(s)\n", self->params.num_destination_textures);
+ return -1;
+ }
+
+ if(load_shader_y(&self->shaders[0], self->params.egl, &self->uniforms[0], params->destination_color, params->color_range, false) != 0) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load Y shader\n");
+ goto err;
+ }
+
+ if(load_shader_uv(&self->shaders[1], self->params.egl, &self->uniforms[1], params->destination_color, params->color_range, false) != 0) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load UV shader\n");
+ goto err;
+ }
+
+ if(self->params.load_external_image_shader) {
+ if(load_shader_y(&self->shaders[2], self->params.egl, &self->uniforms[2], params->destination_color, params->color_range, true) != 0) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load Y shader\n");
+ goto err;
+ }
+
+ if(load_shader_uv(&self->shaders[3], self->params.egl, &self->uniforms[3], params->destination_color, params->color_range, true) != 0) {
+ fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load UV shader\n");
+ goto err;
+ }
+ }
+ break;
+ }
+ }
+
+ if(load_framebuffers(self) != 0)
+ goto err;
+
+ if(create_vertices(self) != 0)
+ goto err;
+
+ return 0;
+
+ err:
+ gsr_color_conversion_deinit(self);
+ return -1;
+}
+
+void gsr_color_conversion_deinit(gsr_color_conversion *self) {
+ if(!self->params.egl)
+ return;
+
+ if(self->vertex_buffer_object_id) {
+ self->params.egl->glDeleteBuffers(1, &self->vertex_buffer_object_id);
+ self->vertex_buffer_object_id = 0;
+ }
+
+ if(self->vertex_array_object_id) {
+ self->params.egl->glDeleteVertexArrays(1, &self->vertex_array_object_id);
+ self->vertex_array_object_id = 0;
+ }
+
+ self->params.egl->glDeleteFramebuffers(MAX_FRAMEBUFFERS, self->framebuffers);
+ for(int i = 0; i < MAX_FRAMEBUFFERS; ++i) {
+ self->framebuffers[i] = 0;
+ }
+
+ for(int i = 0; i < MAX_SHADERS; ++i) {
+ gsr_shader_deinit(&self->shaders[i]);
+ }
+
+ self->params.egl = NULL;
+}
+
+static void gsr_color_conversion_swizzle_texture_source(gsr_color_conversion *self) {
+ if(self->params.source_color == GSR_SOURCE_COLOR_BGR) {
+ const int swizzle_mask[] = { GL_BLUE, GL_GREEN, GL_RED, 1 };
+ self->params.egl->glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, swizzle_mask);
+ }
+}
+
+static void gsr_color_conversion_swizzle_reset(gsr_color_conversion *self) {
+ if(self->params.source_color == GSR_SOURCE_COLOR_BGR) {
+ const int swizzle_mask[] = { GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA };
+ self->params.egl->glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, swizzle_mask);
+ }
+}
+
+/* |source_pos| is in pixel coordinates and |source_size| */
+void gsr_color_conversion_draw(gsr_color_conversion *self, unsigned int texture_id, vec2i source_pos, vec2i source_size, vec2i texture_pos, vec2i texture_size, float rotation, bool external_texture) {
+ // TODO: Remove this crap
+ rotation = M_PI*2.0f - rotation;
+
+ /* TODO: Do not call this every frame? */
+ vec2i dest_texture_size = {0, 0};
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, self->params.destination_textures[0]);
+ self->params.egl->glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &dest_texture_size.x);
+ self->params.egl->glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &dest_texture_size.y);
+ self->params.egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ const int texture_target = external_texture ? GL_TEXTURE_EXTERNAL_OES : GL_TEXTURE_2D;
+
+ self->params.egl->glBindTexture(texture_target, texture_id);
+
+ vec2i source_texture_size = {0, 0};
+ if(external_texture) {
+ source_texture_size = source_size;
+ } else {
+ /* TODO: Do not call this every frame? */
+ self->params.egl->glGetTexLevelParameteriv(texture_target, 0, GL_TEXTURE_WIDTH, &source_texture_size.x);
+ self->params.egl->glGetTexLevelParameteriv(texture_target, 0, GL_TEXTURE_HEIGHT, &source_texture_size.y);
+ }
+
+ // TODO: Remove this crap
+ if(abs_f(M_PI * 0.5f - rotation) <= 0.001f || abs_f(M_PI * 1.5f - rotation) <= 0.001f) {
+ float tmp = source_texture_size.x;
+ source_texture_size.x = source_texture_size.y;
+ source_texture_size.y = tmp;
+ }
+
+ const vec2f pos_norm = {
+ ((float)source_pos.x / (dest_texture_size.x == 0 ? 1.0f : (float)dest_texture_size.x)) * 2.0f,
+ ((float)source_pos.y / (dest_texture_size.y == 0 ? 1.0f : (float)dest_texture_size.y)) * 2.0f,
+ };
+
+ const vec2f size_norm = {
+ ((float)source_size.x / (dest_texture_size.x == 0 ? 1.0f : (float)dest_texture_size.x)) * 2.0f,
+ ((float)source_size.y / (dest_texture_size.y == 0 ? 1.0f : (float)dest_texture_size.y)) * 2.0f,
+ };
+
+ const vec2f texture_pos_norm = {
+ (float)texture_pos.x / (source_texture_size.x == 0 ? 1.0f : (float)source_texture_size.x),
+ (float)texture_pos.y / (source_texture_size.y == 0 ? 1.0f : (float)source_texture_size.y),
+ };
+
+ const vec2f texture_size_norm = {
+ (float)texture_size.x / (source_texture_size.x == 0 ? 1.0f : (float)source_texture_size.x),
+ (float)texture_size.y / (source_texture_size.y == 0 ? 1.0f : (float)source_texture_size.y),
+ };
+
+ const float vertices[] = {
+ -1.0f + 0.0f, -1.0f + 0.0f + size_norm.y, texture_pos_norm.x, texture_pos_norm.y + texture_size_norm.y,
+ -1.0f + 0.0f, -1.0f + 0.0f, texture_pos_norm.x, texture_pos_norm.y,
+ -1.0f + 0.0f + size_norm.x, -1.0f + 0.0f, texture_pos_norm.x + texture_size_norm.x, texture_pos_norm.y,
+
+ -1.0f + 0.0f, -1.0f + 0.0f + size_norm.y, texture_pos_norm.x, texture_pos_norm.y + texture_size_norm.y,
+ -1.0f + 0.0f + size_norm.x, -1.0f + 0.0f, texture_pos_norm.x + texture_size_norm.x, texture_pos_norm.y,
+ -1.0f + 0.0f + size_norm.x, -1.0f + 0.0f + size_norm.y, texture_pos_norm.x + texture_size_norm.x, texture_pos_norm.y + texture_size_norm.y
+ };
+
+ gsr_color_conversion_swizzle_texture_source(self);
+
+ self->params.egl->glBindVertexArray(self->vertex_array_object_id);
+ self->params.egl->glViewport(0, 0, dest_texture_size.x, dest_texture_size.y);
+
+ /* TODO: this, also cleanup */
+ //self->params.egl->glBindBuffer(GL_ARRAY_BUFFER, self->vertex_buffer_object_id);
+ self->params.egl->glBufferSubData(GL_ARRAY_BUFFER, 0, 24 * sizeof(float), vertices);
+
+ {
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[0]);
+ //cap_xcomp->params.egl->glClear(GL_COLOR_BUFFER_BIT); // TODO: Do this in a separate clear_ function. We want to do that when using multiple drm to create the final image (multiple monitors for example)
+
+ const int shader_index = external_texture ? 2 : 0;
+ gsr_shader_use(&self->shaders[shader_index]);
+ self->params.egl->glUniform1f(self->uniforms[shader_index].rotation, rotation);
+ self->params.egl->glUniform2f(self->uniforms[shader_index].offset, pos_norm.x, pos_norm.y);
+ self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6);
+ }
+
+ if(self->params.num_destination_textures > 1) {
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[1]);
+ //cap_xcomp->params.egl->glClear(GL_COLOR_BUFFER_BIT);
+
+ const int shader_index = external_texture ? 3 : 1;
+ gsr_shader_use(&self->shaders[shader_index]);
+ self->params.egl->glUniform1f(self->uniforms[shader_index].rotation, rotation);
+ self->params.egl->glUniform2f(self->uniforms[shader_index].offset, pos_norm.x, pos_norm.y);
+ self->params.egl->glDrawArrays(GL_TRIANGLES, 0, 6);
+ }
+
+ self->params.egl->glBindVertexArray(0);
+ gsr_shader_use_none(&self->shaders[0]);
+ self->params.egl->glBindTexture(texture_target, 0);
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
+
+ gsr_color_conversion_swizzle_reset(self);
+}
+
+void gsr_color_conversion_clear(gsr_color_conversion *self) {
+ float color1[4] = {0.0f, 0.0f, 0.0f, 1.0f};
+ float color2[4] = {0.0f, 0.0f, 0.0f, 1.0f};
+
+ switch(self->params.destination_color) {
+ case GSR_DESTINATION_COLOR_NV12:
+ case GSR_DESTINATION_COLOR_P010: {
+ color2[0] = 0.5f;
+ color2[1] = 0.5f;
+ color2[2] = 0.0f;
+ color2[3] = 1.0f;
+ break;
+ }
+ }
+
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[0]);
+ self->params.egl->glClearColor(color1[0], color1[1], color1[2], color1[3]);
+ self->params.egl->glClear(GL_COLOR_BUFFER_BIT);
+
+ if(self->params.num_destination_textures > 1) {
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, self->framebuffers[1]);
+ self->params.egl->glClearColor(color2[0], color2[1], color2[2], color2[3]);
+ self->params.egl->glClear(GL_COLOR_BUFFER_BIT);
+ }
+
+ self->params.egl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
+}
diff --git a/src/cuda.c b/src/cuda.c
new file mode 100644
index 0000000..6d685b5
--- /dev/null
+++ b/src/cuda.c
@@ -0,0 +1,117 @@
+#include "../include/cuda.h"
+#include "../include/library_loader.h"
+#include <string.h>
+#include <stdio.h>
+#include <dlfcn.h>
+#include <assert.h>
+
+bool gsr_cuda_load(gsr_cuda *self, Display *display, bool do_overclock) {
+ memset(self, 0, sizeof(gsr_cuda));
+ self->do_overclock = do_overclock;
+
+ dlerror(); /* clear */
+ void *lib = dlopen("libcuda.so.1", RTLD_LAZY);
+ if(!lib) {
+ lib = dlopen("libcuda.so", RTLD_LAZY);
+ if(!lib) {
+ fprintf(stderr, "gsr error: gsr_cuda_load failed: failed to load libcuda.so/libcuda.so.1, error: %s\n", dlerror());
+ return false;
+ }
+ }
+
+ dlsym_assign required_dlsym[] = {
+ { (void**)&self->cuInit, "cuInit" },
+ { (void**)&self->cuDeviceGetCount, "cuDeviceGetCount" },
+ { (void**)&self->cuDeviceGet, "cuDeviceGet" },
+ { (void**)&self->cuCtxCreate_v2, "cuCtxCreate_v2" },
+ { (void**)&self->cuCtxDestroy_v2, "cuCtxDestroy_v2" },
+ { (void**)&self->cuCtxPushCurrent_v2, "cuCtxPushCurrent_v2" },
+ { (void**)&self->cuCtxPopCurrent_v2, "cuCtxPopCurrent_v2" },
+ { (void**)&self->cuGetErrorString, "cuGetErrorString" },
+ { (void**)&self->cuMemcpy2D_v2, "cuMemcpy2D_v2" },
+ { (void**)&self->cuMemcpy2DAsync_v2, "cuMemcpy2DAsync_v2" },
+ { (void**)&self->cuStreamSynchronize, "cuStreamSynchronize" },
+
+ { (void**)&self->cuGraphicsGLRegisterImage, "cuGraphicsGLRegisterImage" },
+ { (void**)&self->cuGraphicsEGLRegisterImage, "cuGraphicsEGLRegisterImage" },
+ { (void**)&self->cuGraphicsResourceSetMapFlags, "cuGraphicsResourceSetMapFlags" },
+ { (void**)&self->cuGraphicsMapResources, "cuGraphicsMapResources" },
+ { (void**)&self->cuGraphicsUnmapResources, "cuGraphicsUnmapResources" },
+ { (void**)&self->cuGraphicsUnregisterResource, "cuGraphicsUnregisterResource" },
+ { (void**)&self->cuGraphicsSubResourceGetMappedArray, "cuGraphicsSubResourceGetMappedArray" },
+
+ { NULL, NULL }
+ };
+
+ CUresult res;
+
+ if(!dlsym_load_list(lib, required_dlsym)) {
+ fprintf(stderr, "gsr error: gsr_cuda_load failed: missing required symbols in libcuda.so/libcuda.so.1\n");
+ goto fail;
+ }
+
+ res = self->cuInit(0);
+ if(res != CUDA_SUCCESS) {
+ const char *err_str = "unknown";
+ self->cuGetErrorString(res, &err_str);
+ fprintf(stderr, "gsr error: gsr_cuda_load failed: cuInit failed, error: %s (result: %d)\n", err_str, res);
+ goto fail;
+ }
+
+ int nGpu = 0;
+ self->cuDeviceGetCount(&nGpu);
+ if(nGpu <= 0) {
+ fprintf(stderr, "gsr error: gsr_cuda_load failed: no cuda supported devices found\n");
+ goto fail;
+ }
+
+ CUdevice cu_dev;
+ res = self->cuDeviceGet(&cu_dev, 0);
+ if(res != CUDA_SUCCESS) {
+ const char *err_str = "unknown";
+ self->cuGetErrorString(res, &err_str);
+ fprintf(stderr, "gsr error: gsr_cuda_load failed: unable to get CUDA device, error: %s (result: %d)\n", err_str, res);
+ goto fail;
+ }
+
+ res = self->cuCtxCreate_v2(&self->cu_ctx, CU_CTX_SCHED_AUTO, cu_dev);
+ if(res != CUDA_SUCCESS) {
+ const char *err_str = "unknown";
+ self->cuGetErrorString(res, &err_str);
+ fprintf(stderr, "gsr error: gsr_cuda_load failed: unable to create CUDA context, error: %s (result: %d)\n", err_str, res);
+ goto fail;
+ }
+
+ if(self->do_overclock) {
+ assert(display);
+ if(gsr_overclock_load(&self->overclock, display))
+ gsr_overclock_start(&self->overclock);
+ else
+ fprintf(stderr, "gsr warning: gsr_cuda_load: failed to load xnvctrl, failed to overclock memory transfer rate\n");
+ }
+
+ self->library = lib;
+ return true;
+
+ fail:
+ dlclose(lib);
+ memset(self, 0, sizeof(gsr_cuda));
+ return false;
+}
+
+void gsr_cuda_unload(gsr_cuda *self) {
+ if(self->do_overclock && self->overclock.xnvctrl.library) {
+ gsr_overclock_stop(&self->overclock);
+ gsr_overclock_unload(&self->overclock);
+ }
+
+ if(self->library) {
+ if(self->cu_ctx) {
+ self->cuCtxDestroy_v2(self->cu_ctx);
+ self->cu_ctx = 0;
+ }
+ dlclose(self->library);
+ }
+
+ memset(self, 0, sizeof(gsr_cuda));
+}
diff --git a/src/cursor.c b/src/cursor.c
new file mode 100644
index 0000000..737c33b
--- /dev/null
+++ b/src/cursor.c
@@ -0,0 +1,127 @@
+#include "../include/cursor.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+
+#include <X11/extensions/Xfixes.h>
+
+static bool gsr_cursor_set_from_x11_cursor_image(gsr_cursor *self, XFixesCursorImage *x11_cursor_image) {
+ uint8_t *cursor_data = NULL;
+ uint8_t *out = NULL;
+
+ if(!x11_cursor_image)
+ goto err;
+
+ if(!x11_cursor_image->pixels)
+ goto err;
+
+ self->hotspot.x = x11_cursor_image->xhot;
+ self->hotspot.y = x11_cursor_image->yhot;
+ self->egl->glBindTexture(GL_TEXTURE_2D, self->texture_id);
+
+ self->size.x = x11_cursor_image->width;
+ self->size.y = x11_cursor_image->height;
+ const unsigned long *pixels = x11_cursor_image->pixels;
+ cursor_data = malloc(self->size.x * self->size.y * 4);
+ if(!cursor_data)
+ goto err;
+ out = cursor_data;
+ /* Un-premultiply alpha */
+ for(int y = 0; y < self->size.y; ++y) {
+ for(int x = 0; x < self->size.x; ++x) {
+ uint32_t pixel = *pixels++;
+ uint8_t *in = (uint8_t*)&pixel;
+ uint8_t alpha = in[3];
+ if(alpha == 0)
+ alpha = 1;
+
+ *out++ = (unsigned)*in++ * 255/alpha;
+ *out++ = (unsigned)*in++ * 255/alpha;
+ *out++ = (unsigned)*in++ * 255/alpha;
+ *out++ = *in++;
+ }
+ }
+
+ self->egl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, self->size.x, self->size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, cursor_data);
+ free(cursor_data);
+
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+ XFree(x11_cursor_image);
+ return true;
+
+ err:
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+ if(x11_cursor_image)
+ XFree(x11_cursor_image);
+ return false;
+}
+
+int gsr_cursor_init(gsr_cursor *self, gsr_egl *egl, Display *display) {
+ int x_fixes_error_base = 0;
+
+ assert(egl);
+ assert(display);
+ memset(self, 0, sizeof(*self));
+ self->egl = egl;
+ self->display = display;
+
+ self->x_fixes_event_base = 0;
+ if(!XFixesQueryExtension(self->display, &self->x_fixes_event_base, &x_fixes_error_base)) {
+ fprintf(stderr, "gsr error: gsr_cursor_init: your X11 server is missing the XFixes extension\n");
+ gsr_cursor_deinit(self);
+ return -1;
+ }
+
+ self->egl->glGenTextures(1, &self->texture_id);
+
+ XFixesSelectCursorInput(self->display, DefaultRootWindow(self->display), XFixesDisplayCursorNotifyMask);
+ gsr_cursor_set_from_x11_cursor_image(self, XFixesGetCursorImage(self->display));
+ self->cursor_image_set = true;
+
+ return 0;
+}
+
+void gsr_cursor_deinit(gsr_cursor *self) {
+ if(!self->egl)
+ return;
+
+ if(self->texture_id) {
+ self->egl->glDeleteTextures(1, &self->texture_id);
+ self->texture_id = 0;
+ }
+
+ XFixesSelectCursorInput(self->display, DefaultRootWindow(self->display), 0);
+
+ self->display = NULL;
+ self->egl = NULL;
+}
+
+void gsr_cursor_update(gsr_cursor *self, XEvent *xev) {
+ if(xev->type == self->x_fixes_event_base + XFixesCursorNotify) {
+ XFixesCursorNotifyEvent *cursor_notify_event = (XFixesCursorNotifyEvent*)xev;
+ if(cursor_notify_event->subtype == XFixesDisplayCursorNotify && cursor_notify_event->window == DefaultRootWindow(self->display)) {
+ self->cursor_image_set = true;
+ gsr_cursor_set_from_x11_cursor_image(self, XFixesGetCursorImage(self->display));
+ }
+ }
+
+ if(!self->cursor_image_set) {
+ self->cursor_image_set = true;
+ gsr_cursor_set_from_x11_cursor_image(self, XFixesGetCursorImage(self->display));
+ }
+}
+
+void gsr_cursor_tick(gsr_cursor *self, Window relative_to) {
+ /* TODO: Use XInput2 instead. However that doesn't work when the pointer is grabbed. Maybe check for focused window change and XSelectInput PointerMask */
+ Window dummy_window;
+ int dummy_i;
+ unsigned int dummy_u;
+ XQueryPointer(self->display, relative_to, &dummy_window, &dummy_window, &dummy_i, &dummy_i, &self->position.x, &self->position.y, &dummy_u);
+}
diff --git a/src/egl.c b/src/egl.c
new file mode 100644
index 0000000..2ad4f85
--- /dev/null
+++ b/src/egl.c
@@ -0,0 +1,651 @@
+#include "../include/egl.h"
+#include "../include/library_loader.h"
+#include "../include/utils.h"
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <dlfcn.h>
+#include <assert.h>
+
+#include <wayland-client.h>
+#include <wayland-egl.h>
+#include <unistd.h>
+#include <sys/capability.h>
+
+// TODO: rename gsr_egl to something else since this includes both egl and eglx and in the future maybe vulkan too
+
+// TODO: Move this shit to a separate wayland file, and have a separate file for x11.
+
+static void output_handle_geometry(void *data, struct wl_output *wl_output,
+ int32_t x, int32_t y, int32_t phys_width, int32_t phys_height,
+ int32_t subpixel, const char *make, const char *model,
+ int32_t transform) {
+ (void)wl_output;
+ (void)phys_width;
+ (void)phys_height;
+ (void)subpixel;
+ (void)make;
+ (void)model;
+ gsr_wayland_output *gsr_output = data;
+ gsr_output->pos.x = x;
+ gsr_output->pos.y = y;
+ gsr_output->transform = transform;
+}
+
+static void output_handle_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) {
+ (void)wl_output;
+ (void)flags;
+ (void)refresh;
+ gsr_wayland_output *gsr_output = data;
+ gsr_output->size.x = width;
+ gsr_output->size.y = height;
+}
+
+static void output_handle_done(void *data, struct wl_output *wl_output) {
+ (void)data;
+ (void)wl_output;
+}
+
+static void output_handle_scale(void* data, struct wl_output *wl_output, int32_t factor) {
+ (void)data;
+ (void)wl_output;
+ (void)factor;
+}
+
+static void output_handle_name(void *data, struct wl_output *wl_output, const char *name) {
+ (void)wl_output;
+ gsr_wayland_output *gsr_output = data;
+ if(gsr_output->name) {
+ free(gsr_output->name);
+ gsr_output->name = NULL;
+ }
+ gsr_output->name = strdup(name);
+}
+
+static void output_handle_description(void *data, struct wl_output *wl_output, const char *description) {
+ (void)data;
+ (void)wl_output;
+ (void)description;
+}
+
+static const struct wl_output_listener output_listener = {
+ .geometry = output_handle_geometry,
+ .mode = output_handle_mode,
+ .done = output_handle_done,
+ .scale = output_handle_scale,
+ .name = output_handle_name,
+ .description = output_handle_description,
+};
+
+static void registry_add_object(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
+ (void)version;
+ gsr_egl *egl = data;
+ if (strcmp(interface, "wl_compositor") == 0) {
+ if(egl->wayland.compositor) {
+ wl_compositor_destroy(egl->wayland.compositor);
+ egl->wayland.compositor = NULL;
+ }
+ egl->wayland.compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 1);
+ } else if(strcmp(interface, wl_output_interface.name) == 0) {
+ if(version < 4) {
+ fprintf(stderr, "gsr warning: wl output interface version is < 4, expected >= 4 to capture a monitor. Using KMS capture instead\n");
+ return;
+ }
+
+ if(egl->wayland.num_outputs == GSR_MAX_OUTPUTS) {
+ fprintf(stderr, "gsr warning: reached maximum outputs (32), ignoring output %u\n", name);
+ return;
+ }
+
+ gsr_wayland_output *gsr_output = &egl->wayland.outputs[egl->wayland.num_outputs];
+ egl->wayland.num_outputs++;
+ *gsr_output = (gsr_wayland_output) {
+ .wl_name = name,
+ .output = wl_registry_bind(registry, name, &wl_output_interface, 4),
+ .pos = { .x = 0, .y = 0 },
+ .size = { .x = 0, .y = 0 },
+ .transform = 0,
+ .name = NULL,
+ };
+ wl_output_add_listener(gsr_output->output, &output_listener, gsr_output);
+ }
+}
+
+static void registry_remove_object(void *data, struct wl_registry *registry, uint32_t name) {
+ (void)data;
+ (void)registry;
+ (void)name;
+}
+
+static struct wl_registry_listener registry_listener = {
+ .global = registry_add_object,
+ .global_remove = registry_remove_object,
+};
+
+static void reset_cap_nice(void) {
+ cap_t caps = cap_get_proc();
+ if(!caps)
+ return;
+
+ const cap_value_t cap_to_remove = CAP_SYS_NICE;
+ cap_set_flag(caps, CAP_EFFECTIVE, 1, &cap_to_remove, CAP_CLEAR);
+ cap_set_flag(caps, CAP_PERMITTED, 1, &cap_to_remove, CAP_CLEAR);
+ cap_set_proc(caps);
+ cap_free(caps);
+}
+
+#define GLX_DRAWABLE_TYPE 0x8010
+#define GLX_RENDER_TYPE 0x8011
+#define GLX_RGBA_BIT 0x00000001
+#define GLX_WINDOW_BIT 0x00000001
+#define GLX_PIXMAP_BIT 0x00000002
+#define GLX_BIND_TO_TEXTURE_RGBA_EXT 0x20D1
+#define GLX_BIND_TO_TEXTURE_TARGETS_EXT 0x20D3
+#define GLX_TEXTURE_2D_BIT_EXT 0x00000002
+#define GLX_DOUBLEBUFFER 5
+#define GLX_RED_SIZE 8
+#define GLX_GREEN_SIZE 9
+#define GLX_BLUE_SIZE 10
+#define GLX_ALPHA_SIZE 11
+#define GLX_DEPTH_SIZE 12
+#define GLX_RGBA_TYPE 0x8014
+
+#define GLX_CONTEXT_PRIORITY_LEVEL_EXT 0x3100
+#define GLX_CONTEXT_PRIORITY_HIGH_EXT 0x3101
+#define GLX_CONTEXT_PRIORITY_MEDIUM_EXT 0x3102
+#define GLX_CONTEXT_PRIORITY_LOW_EXT 0x3103
+
+static GLXFBConfig glx_fb_config_choose(gsr_egl *self) {
+ const int glx_visual_attribs[] = {
+ GLX_RENDER_TYPE, GLX_RGBA_BIT,
+ GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT,
+ // TODO:
+ //GLX_BIND_TO_TEXTURE_RGBA_EXT, 1,
+ //GLX_BIND_TO_TEXTURE_TARGETS_EXT, GLX_TEXTURE_2D_BIT_EXT,
+ GLX_DOUBLEBUFFER, True,
+ GLX_RED_SIZE, 8,
+ GLX_GREEN_SIZE, 8,
+ GLX_BLUE_SIZE, 8,
+ GLX_ALPHA_SIZE, 0,
+ GLX_DEPTH_SIZE, 0,
+ None, None
+ };
+
+ // TODO: Cleanup
+ int c = 0;
+ GLXFBConfig *fb_configs = self->glXChooseFBConfig(self->x11.dpy, DefaultScreen(self->x11.dpy), glx_visual_attribs, &c);
+ if(c == 0 || !fb_configs)
+ return NULL;
+
+ return fb_configs[0];
+}
+
+// TODO: Create egl context without surface (in other words, x11/wayland agnostic, doesn't require x11/wayland dependency)
+static bool gsr_egl_create_window(gsr_egl *self, bool wayland) {
+ EGLConfig ecfg;
+ int32_t num_config = 0;
+
+ const int32_t attr[] = {
+ EGL_BUFFER_SIZE, 24,
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
+ EGL_NONE, EGL_NONE
+ };
+
+ const int32_t ctxattr[] = {
+ EGL_CONTEXT_CLIENT_VERSION, 2,
+ EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, /* requires cap_sys_nice, ignored otherwise */
+ EGL_NONE, EGL_NONE
+ };
+
+ if(wayland) {
+ self->wayland.dpy = wl_display_connect(NULL);
+ if(!self->wayland.dpy) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: wl_display_connect failed\n");
+ goto fail;
+ }
+
+ self->wayland.registry = wl_display_get_registry(self->wayland.dpy); // TODO: Error checking
+ wl_registry_add_listener(self->wayland.registry, &registry_listener, self); // TODO: Error checking
+
+ // Fetch globals
+ wl_display_roundtrip(self->wayland.dpy);
+
+ // Fetch wl_output
+ wl_display_roundtrip(self->wayland.dpy);
+
+ if(!self->wayland.compositor) {
+ fprintf(stderr, "gsr error: gsr_gl_create_window failed: failed to find compositor\n");
+ goto fail;
+ }
+ } else {
+ self->x11.window = XCreateWindow(self->x11.dpy, DefaultRootWindow(self->x11.dpy), 0, 0, 16, 16, 0, CopyFromParent, InputOutput, CopyFromParent, 0, NULL);
+
+ if(!self->x11.window) {
+ fprintf(stderr, "gsr error: gsr_gl_create_window failed: failed to create gl window\n");
+ goto fail;
+ }
+ }
+
+ self->eglBindAPI(EGL_OPENGL_API);
+
+ self->egl_display = self->eglGetDisplay(self->wayland.dpy ? (EGLNativeDisplayType)self->wayland.dpy : (EGLNativeDisplayType)self->x11.dpy);
+ if(!self->egl_display) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: eglGetDisplay failed\n");
+ goto fail;
+ }
+
+ if(!self->eglInitialize(self->egl_display, NULL, NULL)) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: eglInitialize failed\n");
+ goto fail;
+ }
+
+ if(!self->eglChooseConfig(self->egl_display, attr, &ecfg, 1, &num_config) || num_config != 1) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to find a matching config\n");
+ goto fail;
+ }
+
+ self->egl_context = self->eglCreateContext(self->egl_display, ecfg, NULL, ctxattr);
+ if(!self->egl_context) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create egl context\n");
+ goto fail;
+ }
+
+ if(wayland) {
+ self->wayland.surface = wl_compositor_create_surface(self->wayland.compositor);
+ self->wayland.window = wl_egl_window_create(self->wayland.surface, 16, 16);
+ self->egl_surface = self->eglCreateWindowSurface(self->egl_display, ecfg, (EGLNativeWindowType)self->wayland.window, NULL);
+ } else {
+ self->egl_surface = self->eglCreateWindowSurface(self->egl_display, ecfg, (EGLNativeWindowType)self->x11.window, NULL);
+ }
+
+ if(!self->egl_surface) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create window surface\n");
+ goto fail;
+ }
+
+ if(!self->eglMakeCurrent(self->egl_display, self->egl_surface, self->egl_surface, self->egl_context)) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to make egl context current\n");
+ goto fail;
+ }
+
+ reset_cap_nice();
+ return true;
+
+ fail:
+ reset_cap_nice();
+ gsr_egl_unload(self);
+ return false;
+}
+
+static bool gsr_egl_switch_to_glx_context(gsr_egl *self) {
+ // TODO: Cleanup
+
+ if(self->egl_context) {
+ self->eglMakeCurrent(self->egl_display, NULL, NULL, NULL);
+ self->eglDestroyContext(self->egl_display, self->egl_context);
+ self->egl_context = NULL;
+ }
+
+ if(self->egl_surface) {
+ self->eglDestroySurface(self->egl_display, self->egl_surface);
+ self->egl_surface = NULL;
+ }
+
+ if(self->egl_display) {
+ self->eglTerminate(self->egl_display);
+ self->egl_display = NULL;
+ }
+
+ self->glx_fb_config = glx_fb_config_choose(self);
+ if(!self->glx_fb_config) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to find a suitable fb config\n");
+ goto fail;
+ }
+
+ // TODO:
+ //self->glx_context = self->glXCreateContextAttribsARB(self->x11.dpy, self->glx_fb_config, NULL, True, context_attrib_list);
+ self->glx_context = self->glXCreateNewContext(self->x11.dpy, self->glx_fb_config, GLX_RGBA_TYPE, NULL, True);
+ if(!self->glx_context) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create glx context\n");
+ goto fail;
+ }
+
+ if(!self->glXMakeContextCurrent(self->x11.dpy, self->x11.window, self->x11.window, self->glx_context)) {
+ fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to make glx context current\n");
+ goto fail;
+ }
+
+ return true;
+
+ fail:
+ if(self->glx_context) {
+ self->glXMakeContextCurrent(self->x11.dpy, None, None, NULL);
+ self->glXDestroyContext(self->x11.dpy, self->glx_context);
+ self->glx_context = NULL;
+ self->glx_fb_config = NULL;
+ }
+ return false;
+}
+
+static bool gsr_egl_load_egl(gsr_egl *self, void *library) {
+ dlsym_assign required_dlsym[] = {
+ { (void**)&self->eglGetError, "eglGetError" },
+ { (void**)&self->eglGetDisplay, "eglGetDisplay" },
+ { (void**)&self->eglInitialize, "eglInitialize" },
+ { (void**)&self->eglTerminate, "eglTerminate" },
+ { (void**)&self->eglChooseConfig, "eglChooseConfig" },
+ { (void**)&self->eglCreateWindowSurface, "eglCreateWindowSurface" },
+ { (void**)&self->eglCreateContext, "eglCreateContext" },
+ { (void**)&self->eglMakeCurrent, "eglMakeCurrent" },
+ { (void**)&self->eglCreateImage, "eglCreateImage" },
+ { (void**)&self->eglDestroyContext, "eglDestroyContext" },
+ { (void**)&self->eglDestroySurface, "eglDestroySurface" },
+ { (void**)&self->eglDestroyImage, "eglDestroyImage" },
+ { (void**)&self->eglSwapInterval, "eglSwapInterval" },
+ { (void**)&self->eglSwapBuffers, "eglSwapBuffers" },
+ { (void**)&self->eglBindAPI, "eglBindAPI" },
+ { (void**)&self->eglGetProcAddress, "eglGetProcAddress" },
+
+ { NULL, NULL }
+ };
+
+ if(!dlsym_load_list(library, required_dlsym)) {
+ fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libEGL.so.1\n");
+ return false;
+ }
+
+ return true;
+}
+
+static bool gsr_egl_proc_load_egl(gsr_egl *self) {
+ self->eglExportDMABUFImageQueryMESA = (FUNC_eglExportDMABUFImageQueryMESA)self->eglGetProcAddress("eglExportDMABUFImageQueryMESA");
+ self->eglExportDMABUFImageMESA = (FUNC_eglExportDMABUFImageMESA)self->eglGetProcAddress("eglExportDMABUFImageMESA");
+ self->glEGLImageTargetTexture2DOES = (FUNC_glEGLImageTargetTexture2DOES)self->eglGetProcAddress("glEGLImageTargetTexture2DOES");
+ self->eglQueryDisplayAttribEXT = (FUNC_eglQueryDisplayAttribEXT)self->eglGetProcAddress("eglQueryDisplayAttribEXT");
+ self->eglQueryDeviceStringEXT = (FUNC_eglQueryDeviceStringEXT)self->eglGetProcAddress("eglQueryDeviceStringEXT");
+
+ if(!self->glEGLImageTargetTexture2DOES) {
+ fprintf(stderr, "gsr error: gsr_egl_load failed: could not find glEGLImageTargetTexture2DOES\n");
+ return false;
+ }
+
+ return true;
+}
+
+static bool gsr_egl_load_glx(gsr_egl *self, void *library) {
+ dlsym_assign required_dlsym[] = {
+ { (void**)&self->glXGetProcAddress, "glXGetProcAddress" },
+ { (void**)&self->glXChooseFBConfig, "glXChooseFBConfig" },
+ { (void**)&self->glXMakeContextCurrent, "glXMakeContextCurrent" },
+ { (void**)&self->glXCreateNewContext, "glXCreateNewContext" },
+ { (void**)&self->glXDestroyContext, "glXDestroyContext" },
+ { (void**)&self->glXSwapBuffers, "glXSwapBuffers" },
+
+ { NULL, NULL }
+ };
+
+ if(!dlsym_load_list(library, required_dlsym)) {
+ fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libGLX.so.0\n");
+ return false;
+ }
+
+ self->glXCreateContextAttribsARB = (FUNC_glXCreateContextAttribsARB)self->glXGetProcAddress((const unsigned char*)"glXCreateContextAttribsARB");
+ if(!self->glXCreateContextAttribsARB) {
+ fprintf(stderr, "gsr error: gsr_egl_load_glx failed: could not find glXCreateContextAttribsARB\n");
+ return false;
+ }
+
+ self->glXSwapIntervalEXT = (FUNC_glXSwapIntervalEXT)self->glXGetProcAddress((const unsigned char*)"glXSwapIntervalEXT");
+ self->glXSwapIntervalMESA = (FUNC_glXSwapIntervalMESA)self->glXGetProcAddress((const unsigned char*)"glXSwapIntervalMESA");
+ self->glXSwapIntervalSGI = (FUNC_glXSwapIntervalSGI)self->glXGetProcAddress((const unsigned char*)"glXSwapIntervalSGI");
+
+ return true;
+}
+
+static bool gsr_egl_load_gl(gsr_egl *self, void *library) {
+ dlsym_assign required_dlsym[] = {
+ { (void**)&self->glGetError, "glGetError" },
+ { (void**)&self->glGetString, "glGetString" },
+ { (void**)&self->glFlush, "glFlush" },
+ { (void**)&self->glFinish, "glFinish" },
+ { (void**)&self->glClear, "glClear" },
+ { (void**)&self->glClearColor, "glClearColor" },
+ { (void**)&self->glGenTextures, "glGenTextures" },
+ { (void**)&self->glDeleteTextures, "glDeleteTextures" },
+ { (void**)&self->glBindTexture, "glBindTexture" },
+ { (void**)&self->glTexParameteri, "glTexParameteri" },
+ { (void**)&self->glTexParameteriv, "glTexParameteriv" },
+ { (void**)&self->glGetTexLevelParameteriv, "glGetTexLevelParameteriv" },
+ { (void**)&self->glTexImage2D, "glTexImage2D" },
+ { (void**)&self->glCopyImageSubData, "glCopyImageSubData" },
+ { (void**)&self->glClearTexImage, "glClearTexImage" },
+ { (void**)&self->glGenFramebuffers, "glGenFramebuffers" },
+ { (void**)&self->glBindFramebuffer, "glBindFramebuffer" },
+ { (void**)&self->glDeleteFramebuffers, "glDeleteFramebuffers" },
+ { (void**)&self->glViewport, "glViewport" },
+ { (void**)&self->glFramebufferTexture2D, "glFramebufferTexture2D" },
+ { (void**)&self->glDrawBuffers, "glDrawBuffers" },
+ { (void**)&self->glCheckFramebufferStatus, "glCheckFramebufferStatus" },
+ { (void**)&self->glBindBuffer, "glBindBuffer" },
+ { (void**)&self->glGenBuffers, "glGenBuffers" },
+ { (void**)&self->glBufferData, "glBufferData" },
+ { (void**)&self->glBufferSubData, "glBufferSubData" },
+ { (void**)&self->glDeleteBuffers, "glDeleteBuffers" },
+ { (void**)&self->glGenVertexArrays, "glGenVertexArrays" },
+ { (void**)&self->glBindVertexArray, "glBindVertexArray" },
+ { (void**)&self->glDeleteVertexArrays, "glDeleteVertexArrays" },
+ { (void**)&self->glCreateProgram, "glCreateProgram" },
+ { (void**)&self->glCreateShader, "glCreateShader" },
+ { (void**)&self->glAttachShader, "glAttachShader" },
+ { (void**)&self->glBindAttribLocation, "glBindAttribLocation" },
+ { (void**)&self->glCompileShader, "glCompileShader" },
+ { (void**)&self->glLinkProgram, "glLinkProgram" },
+ { (void**)&self->glShaderSource, "glShaderSource" },
+ { (void**)&self->glUseProgram, "glUseProgram" },
+ { (void**)&self->glGetProgramInfoLog, "glGetProgramInfoLog" },
+ { (void**)&self->glGetShaderiv, "glGetShaderiv" },
+ { (void**)&self->glGetShaderInfoLog, "glGetShaderInfoLog" },
+ { (void**)&self->glDeleteProgram, "glDeleteProgram" },
+ { (void**)&self->glDeleteShader, "glDeleteShader" },
+ { (void**)&self->glGetProgramiv, "glGetProgramiv" },
+ { (void**)&self->glVertexAttribPointer, "glVertexAttribPointer" },
+ { (void**)&self->glEnableVertexAttribArray, "glEnableVertexAttribArray" },
+ { (void**)&self->glDrawArrays, "glDrawArrays" },
+ { (void**)&self->glEnable, "glEnable" },
+ { (void**)&self->glBlendFunc, "glBlendFunc" },
+ { (void**)&self->glGetUniformLocation, "glGetUniformLocation" },
+ { (void**)&self->glUniform1f, "glUniform1f" },
+ { (void**)&self->glUniform2f, "glUniform2f" },
+ { (void**)&self->glDebugMessageCallback, "glDebugMessageCallback" },
+
+ { NULL, NULL }
+ };
+
+ if(!dlsym_load_list(library, required_dlsym)) {
+ fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libGL.so.1\n");
+ return false;
+ }
+
+ return true;
+}
+
+// #define GL_DEBUG_TYPE_ERROR 0x824C
+// static void debug_callback( unsigned int source,
+// unsigned int type,
+// unsigned int id,
+// unsigned int severity,
+// int length,
+// const char* message,
+// const void* userParam )
+// {
+// (void)source;
+// (void)id;
+// (void)length;
+// (void)userParam;
+// fprintf( stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
+// ( type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "" ),
+// type, severity, message );
+// }
+
+bool gsr_egl_load(gsr_egl *self, Display *dpy, bool wayland, bool is_monitor_capture) {
+ (void)is_monitor_capture;
+ memset(self, 0, sizeof(gsr_egl));
+ self->x11.dpy = dpy;
+ self->context_type = GSR_GL_CONTEXT_TYPE_EGL;
+
+ dlerror(); /* clear */
+ self->egl_library = dlopen("libEGL.so.1", RTLD_LAZY);
+ if(!self->egl_library) {
+ fprintf(stderr, "gsr error: gsr_egl_load: failed to load libEGL.so.1, error: %s\n", dlerror());
+ goto fail;
+ }
+
+ self->glx_library = dlopen("libGLX.so.0", RTLD_LAZY);
+ if(!self->glx_library) {
+ fprintf(stderr, "gsr error: gsr_egl_load: failed to load libGLX.so.0, error: %s\n", dlerror());
+ goto fail;
+ }
+
+ self->gl_library = dlopen("libGL.so.1", RTLD_LAZY);
+ if(!self->egl_library) {
+ fprintf(stderr, "gsr error: gsr_egl_load: failed to load libGL.so.1, error: %s\n", dlerror());
+ goto fail;
+ }
+
+ if(!gsr_egl_load_egl(self, self->egl_library))
+ goto fail;
+
+ if(!gsr_egl_load_glx(self, self->glx_library))
+ goto fail;
+
+ if(!gsr_egl_load_gl(self, self->gl_library))
+ goto fail;
+
+ if(!gsr_egl_proc_load_egl(self))
+ goto fail;
+
+ if(!gsr_egl_create_window(self, wayland))
+ goto fail;
+
+ if(!gl_get_gpu_info(self, &self->gpu_info))
+ goto fail;
+
+ if(self->eglQueryDisplayAttribEXT && self->eglQueryDeviceStringEXT) {
+ intptr_t device = 0;
+ if(self->eglQueryDisplayAttribEXT(self->egl_display, EGL_DEVICE_EXT, &device) && device)
+ self->dri_card_path = self->eglQueryDeviceStringEXT((void*)device, EGL_DRM_DEVICE_FILE_EXT);
+ }
+
+ /* Nvfbc requires glx */
+ if(!wayland && is_monitor_capture && self->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) {
+ self->context_type = GSR_GL_CONTEXT_TYPE_GLX;
+ self->dri_card_path = NULL;
+ if(!gsr_egl_switch_to_glx_context(self))
+ goto fail;
+ }
+
+ self->glEnable(GL_BLEND);
+ self->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ //self->glEnable(GL_DEBUG_OUTPUT);
+ //self->glDebugMessageCallback(debug_callback, NULL);
+
+ return true;
+
+ fail:
+ gsr_egl_unload(self);
+ return false;
+}
+
+void gsr_egl_unload(gsr_egl *self) {
+ if(self->egl_context) {
+ self->eglMakeCurrent(self->egl_display, NULL, NULL, NULL);
+ self->eglDestroyContext(self->egl_display, self->egl_context);
+ self->egl_context = NULL;
+ }
+
+ if(self->egl_surface) {
+ self->eglDestroySurface(self->egl_display, self->egl_surface);
+ self->egl_surface = NULL;
+ }
+
+ if(self->egl_display) {
+ self->eglTerminate(self->egl_display);
+ self->egl_display = NULL;
+ }
+
+ if(self->glx_context) {
+ self->glXMakeContextCurrent(self->x11.dpy, None, None, NULL);
+ self->glXDestroyContext(self->x11.dpy, self->glx_context);
+ self->glx_context = NULL;
+ self->glx_fb_config = NULL;
+ }
+
+ if(self->x11.window) {
+ XDestroyWindow(self->x11.dpy, self->x11.window);
+ self->x11.window = None;
+ }
+
+ if(self->wayland.window) {
+ wl_egl_window_destroy(self->wayland.window);
+ self->wayland.window = NULL;
+ }
+
+ if(self->wayland.surface) {
+ wl_surface_destroy(self->wayland.surface);
+ self->wayland.surface = NULL;
+ }
+
+ for(int i = 0; i < self->wayland.num_outputs; ++i) {
+ if(self->wayland.outputs[i].output) {
+ wl_output_destroy(self->wayland.outputs[i].output);
+ self->wayland.outputs[i].output = NULL;
+ }
+
+ if(self->wayland.outputs[i].name) {
+ free(self->wayland.outputs[i].name);
+ self->wayland.outputs[i].name = NULL;
+ }
+ }
+ self->wayland.num_outputs = 0;
+
+ if(self->wayland.compositor) {
+ wl_compositor_destroy(self->wayland.compositor);
+ self->wayland.compositor = NULL;
+ }
+
+ if(self->wayland.registry) {
+ wl_registry_destroy(self->wayland.registry);
+ self->wayland.registry = NULL;
+ }
+
+ if(self->wayland.dpy) {
+ wl_display_disconnect(self->wayland.dpy);
+ self->wayland.dpy = NULL;
+ }
+
+ if(self->egl_library) {
+ dlclose(self->egl_library);
+ self->egl_library = NULL;
+ }
+
+ if(self->glx_library) {
+ dlclose(self->glx_library);
+ self->glx_library = NULL;
+ }
+
+ if(self->gl_library) {
+ dlclose(self->gl_library);
+ self->gl_library = NULL;
+ }
+
+ memset(self, 0, sizeof(gsr_egl));
+}
+
+void gsr_egl_update(gsr_egl *self) {
+ if(!self->wayland.dpy)
+ return;
+
+ // TODO: pselect on wl_display_get_fd before doing dispatch
+ wl_display_dispatch(self->wayland.dpy);
+}
diff --git a/src/library_loader.c b/src/library_loader.c
new file mode 100644
index 0000000..0aeee9b
--- /dev/null
+++ b/src/library_loader.c
@@ -0,0 +1,34 @@
+#include "../include/library_loader.h"
+
+#include <dlfcn.h>
+#include <stdbool.h>
+#include <stdio.h>
+
+void* dlsym_print_fail(void *handle, const char *name, bool required) {
+ dlerror();
+ void *sym = dlsym(handle, name);
+ char *err_str = dlerror();
+
+ if(!sym)
+ fprintf(stderr, "%s: dlsym(handle, \"%s\") failed, error: %s\n", required ? "error" : "warning", name, err_str ? err_str : "(null)");
+
+ return sym;
+}
+
+/* |dlsyms| should be null terminated */
+bool dlsym_load_list(void *handle, const dlsym_assign *dlsyms) {
+ bool success = true;
+ for(int i = 0; dlsyms[i].func; ++i) {
+ *dlsyms[i].func = dlsym_print_fail(handle, dlsyms[i].name, true);
+ if(!*dlsyms[i].func)
+ success = false;
+ }
+ return success;
+}
+
+/* |dlsyms| should be null terminated */
+void dlsym_load_list_optional(void *handle, const dlsym_assign *dlsyms) {
+ for(int i = 0; dlsyms[i].func; ++i) {
+ *dlsyms[i].func = dlsym_print_fail(handle, dlsyms[i].name, false);
+ }
+}
diff --git a/src/main.cpp b/src/main.cpp
index 2b29d3b..98740c3 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,419 +1,332 @@
-/*
- Copyright (C) 2020 dec05eba
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
+extern "C" {
+#include "../include/capture/nvfbc.h"
+#include "../include/capture/xcomposite_cuda.h"
+#include "../include/capture/xcomposite_vaapi.h"
+#include "../include/capture/kms_vaapi.h"
+#include "../include/capture/kms_cuda.h"
+#include "../include/egl.h"
+#include "../include/utils.h"
+#include "../include/color_conversion.h"
+}
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <vector>
+#include <unordered_map>
#include <thread>
#include <mutex>
#include <map>
#include <signal.h>
#include <sys/stat.h>
-
#include <unistd.h>
-#include <fcntl.h>
+#include <sys/wait.h>
+#include <libgen.h>
#include "../include/sound.hpp"
-#define GLX_GLXEXT_PROTOTYPES
-#include <GL/glew.h>
-#include <GL/glx.h>
-#include <GL/glxext.h>
-#include <GLFW/glfw3.h>
-
-#include <X11/extensions/Xcomposite.h>
-
extern "C" {
#include <libavutil/pixfmt.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
-#include <libavutil/hwcontext.h>
-#include <libavutil/hwcontext_cuda.h>
#include <libavutil/opt.h>
#include <libswresample/swresample.h>
#include <libavutil/avutil.h>
#include <libavutil/time.h>
-}
-#include <cudaGL.h>
-
-extern "C" {
-#include <libavutil/hwcontext.h>
+#include <libavfilter/avfilter.h>
+#include <libavfilter/buffersink.h>
+#include <libavfilter/buffersrc.h>
}
-#include "../include/NvFBCLibrary.hpp"
-
#include <deque>
#include <future>
-//#include <CL/cl.h>
+// TODO: If options are not supported then they are returned (allocated) in the options. This should be free'd.
+
+// TODO: Remove LIBAVUTIL_VERSION_MAJOR checks in the future when ubuntu, pop os LTS etc update ffmpeg to >= 5.0
static const int VIDEO_STREAM_INDEX = 0;
-static const int AUDIO_STREAM_INDEX = 1;
static thread_local char av_error_buffer[AV_ERROR_MAX_STRING_SIZE];
+static void monitor_output_callback_print(const gsr_monitor *monitor, void *userdata) {
+ (void)userdata;
+ fprintf(stderr, " \"%.*s\" (%dx%d+%d+%d)\n", monitor->name_len, monitor->name, monitor->size.x, monitor->size.y, monitor->pos.x, monitor->pos.y);
+}
+
+typedef struct {
+ const char *output_name;
+} FirstOutputCallback;
+
+static void get_first_output(const gsr_monitor *monitor, void *userdata) {
+ FirstOutputCallback *first_output = (FirstOutputCallback*)userdata;
+ if(!first_output->output_name)
+ first_output->output_name = strndup(monitor->name, monitor->name_len + 1);
+}
+
static char* av_error_to_string(int err) {
if(av_strerror(err, av_error_buffer, sizeof(av_error_buffer)) < 0)
strcpy(av_error_buffer, "Unknown error");
return av_error_buffer;
}
-struct ScopedGLXFBConfig {
- ~ScopedGLXFBConfig() {
- if (configs)
- XFree(configs);
- }
-
- GLXFBConfig *configs = nullptr;
-};
-
-struct WindowPixmap {
- WindowPixmap()
- : pixmap(None), glx_pixmap(None), texture_id(0), target_texture_id(0),
- texture_width(0), texture_height(0) {}
-
- Pixmap pixmap;
- GLXPixmap glx_pixmap;
- GLuint texture_id;
- GLuint target_texture_id;
-
- GLint texture_width;
- GLint texture_height;
-};
-
enum class VideoQuality {
MEDIUM,
HIGH,
+ VERY_HIGH,
ULTRA
};
-static double clock_get_monotonic_seconds() {
- struct timespec ts;
- ts.tv_sec = 0;
- ts.tv_nsec = 0;
- clock_gettime(CLOCK_MONOTONIC, &ts);
- return (double)ts.tv_sec + (double)ts.tv_nsec * 0.000000001;
-}
+enum class VideoCodec {
+ H264,
+ HEVC,
+ HEVC_HDR,
+ AV1,
+ AV1_HDR
+};
-static bool x11_supports_composite_named_window_pixmap(Display *dpy) {
- int extension_major;
- int extension_minor;
- if (!XCompositeQueryExtension(dpy, &extension_major, &extension_minor))
- return false;
+enum class AudioCodec {
+ AAC,
+ OPUS,
+ FLAC
+};
- int major_version;
- int minor_version;
- return XCompositeQueryVersion(dpy, &major_version, &minor_version) &&
- (major_version > 0 || minor_version >= 2);
-}
+enum class PixelFormat {
+ YUV420,
+ YUV444
+};
-static int x11_error_handler(Display *dpy, XErrorEvent *ev) {
-#if 0
- char type_str[128];
- XGetErrorText(dpy, ev->type, type_str, sizeof(type_str));
-
- char major_opcode_str[128];
- XGetErrorText(dpy, ev->type, major_opcode_str, sizeof(major_opcode_str));
-
- char minor_opcode_str[128];
- XGetErrorText(dpy, ev->type, minor_opcode_str, sizeof(minor_opcode_str));
-
- fprintf(stderr,
- "X Error of failed request: %s\n"
- "Major opcode of failed request: %d (%s)\n"
- "Minor opcode of failed request: %d (%s)\n"
- "Serial number of failed request: %d\n",
- type_str,
- ev->request_code, major_opcode_str,
- ev->minor_code, minor_opcode_str);
-#endif
+enum class FramerateMode {
+ CONSTANT,
+ VARIABLE
+};
+
+static int x11_error_handler(Display*, XErrorEvent*) {
return 0;
}
-static int x11_io_error_handler(Display *dpy) {
+static int x11_io_error_handler(Display*) {
return 0;
}
-static void cleanup_window_pixmap(Display *dpy, WindowPixmap &pixmap) {
- if (pixmap.target_texture_id) {
- glDeleteTextures(1, &pixmap.target_texture_id);
- pixmap.target_texture_id = 0;
- }
-
- if (pixmap.texture_id) {
- glDeleteTextures(1, &pixmap.texture_id);
- pixmap.texture_id = 0;
- pixmap.texture_width = 0;
- pixmap.texture_height = 0;
- }
-
- if (pixmap.glx_pixmap) {
- glXDestroyPixmap(dpy, pixmap.glx_pixmap);
- glXReleaseTexImageEXT(dpy, pixmap.glx_pixmap, GLX_FRONT_EXT);
- pixmap.glx_pixmap = None;
- }
-
- if (pixmap.pixmap) {
- XFreePixmap(dpy, pixmap.pixmap);
- pixmap.pixmap = None;
+static bool video_codec_is_hdr(VideoCodec video_codec) {
+ switch(video_codec) {
+ case VideoCodec::HEVC_HDR:
+ case VideoCodec::AV1_HDR:
+ return true;
+ default:
+ return false;
}
}
-static bool recreate_window_pixmap(Display *dpy, Window window_id,
- WindowPixmap &pixmap) {
- cleanup_window_pixmap(dpy, pixmap);
-
- XWindowAttributes attr;
- if (!XGetWindowAttributes(dpy, window_id, &attr)) {
- fprintf(stderr, "Failed to get window attributes\n");
- return false;
- }
+struct PacketData {
+ PacketData() {}
+ PacketData(const PacketData&) = delete;
+ PacketData& operator=(const PacketData&) = delete;
- const int pixmap_config[] = {
- GLX_BIND_TO_TEXTURE_RGB_EXT, True,
- GLX_DRAWABLE_TYPE, GLX_PIXMAP_BIT | GLX_WINDOW_BIT,
- GLX_BIND_TO_TEXTURE_TARGETS_EXT, GLX_TEXTURE_2D_BIT_EXT,
- GLX_BUFFER_SIZE, 24,
- GLX_RED_SIZE, 8,
- GLX_GREEN_SIZE, 8,
- GLX_BLUE_SIZE, 8,
- GLX_ALPHA_SIZE, 0,
- // GLX_Y_INVERTED_EXT, (int)GLX_DONT_CARE,
- None};
-
- const int pixmap_attribs[] = {GLX_TEXTURE_TARGET_EXT,
- GLX_TEXTURE_2D_EXT,
- GLX_TEXTURE_FORMAT_EXT,
- GLX_TEXTURE_FORMAT_RGB_EXT,
- None};
-
- int c;
- GLXFBConfig *configs = glXChooseFBConfig(dpy, 0, pixmap_config, &c);
- if (!configs) {
- fprintf(stderr, "Failed too choose fb config\n");
- return false;
- }
- ScopedGLXFBConfig scoped_configs;
- scoped_configs.configs = configs;
-
- bool found = false;
- GLXFBConfig config;
- for (int i = 0; i < c; i++) {
- config = configs[i];
- XVisualInfo *visual = glXGetVisualFromFBConfig(dpy, config);
- if (!visual)
- continue;
-
- if (attr.depth != visual->depth) {
- XFree(visual);
- continue;
- }
- XFree(visual);
- found = true;
- break;
+ ~PacketData() {
+ av_free(data.data);
}
- if(!found) {
- fprintf(stderr, "No matching fb config found\n");
- return false;
- }
-
- Pixmap new_window_pixmap = XCompositeNameWindowPixmap(dpy, window_id);
- if (!new_window_pixmap) {
- fprintf(stderr, "Failed to get pixmap for window %ld\n", window_id);
- return false;
- }
-
- GLXPixmap glx_pixmap =
- glXCreatePixmap(dpy, config, new_window_pixmap, pixmap_attribs);
- if (!glx_pixmap) {
- fprintf(stderr, "Failed to create glx pixmap\n");
- XFreePixmap(dpy, new_window_pixmap);
- return false;
- }
-
- pixmap.pixmap = new_window_pixmap;
- pixmap.glx_pixmap = glx_pixmap;
-
- //glEnable(GL_TEXTURE_2D);
- glGenTextures(1, &pixmap.texture_id);
- glBindTexture(GL_TEXTURE_2D, pixmap.texture_id);
-
- // glEnable(GL_BLEND);
- // glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-
- glXBindTexImageEXT(dpy, pixmap.glx_pixmap, GLX_FRONT_EXT, NULL);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
- GL_NEAREST); // GL_LINEAR );
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
- GL_NEAREST); // GL_LINEAR);//GL_LINEAR_MIPMAP_LINEAR );
- //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
-
- glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH,
- &pixmap.texture_width);
- glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT,
- &pixmap.texture_height);
-
- if(pixmap.texture_width == 0 || pixmap.texture_height == 0) {
- pixmap.texture_width = attr.width;
- pixmap.texture_height = attr.height;
- fprintf(stderr, "Warning: failed to get texture size. You are probably running an unsupported compositor and recording the selected window doesn't work at the moment. This could also happen if you are trying to record a window with client-side decorations (GNOME issue). A black window will be displayed instead. A workaround is to record the whole monitor (which use NvFBC).\n");
- }
-
- fprintf(stderr, "texture width: %d, height: %d\n", pixmap.texture_width,
- pixmap.texture_height);
-
- // Generating this second texture is needed because
- // cuGraphicsGLRegisterImage cant be used with the texture that is mapped
- // directly to the pixmap.
- // TODO: Investigate if it's somehow possible to use the pixmap texture
- // directly, this should improve performance since only less image copy is
- // then needed every frame.
- glGenTextures(1, &pixmap.target_texture_id);
- glBindTexture(GL_TEXTURE_2D, pixmap.target_texture_id);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, pixmap.texture_width,
- pixmap.texture_height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
- int err2 = glGetError();
- //fprintf(stderr, "error: %d\n", err2);
- // glXBindTexImageEXT(dpy, pixmap.glx_pixmap, GLX_FRONT_EXT, NULL);
- // glGenerateTextureMipmapEXT(glxpixmap, GL_TEXTURE_2D);
-
- // glGenerateMipmap(GL_TEXTURE_2D);
-
- // glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
- // glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
-
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
- GL_NEAREST); // GL_LINEAR );
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
- GL_NEAREST); // GL_LINEAR);//GL_LINEAR_MIPMAP_LINEAR );
- //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
-
- glBindTexture(GL_TEXTURE_2D, 0);
-
- return pixmap.texture_id != 0 && pixmap.target_texture_id != 0;
-}
+ AVPacket data;
+};
// |stream| is only required for non-replay mode
-static void receive_frames(AVCodecContext *av_codec_context, int stream_index, AVStream *stream, AVFrame *frame,
+static void receive_frames(AVCodecContext *av_codec_context, int stream_index, AVStream *stream, int64_t pts,
AVFormatContext *av_format_context,
double replay_start_time,
- std::deque<AVPacket> &frame_data_queue,
+ std::deque<std::shared_ptr<PacketData>> &frame_data_queue,
int replay_buffer_size_secs,
bool &frames_erased,
- std::mutex &write_output_mutex) {
- AVPacket av_packet;
- memset(&av_packet, 0, sizeof(av_packet));
+ std::mutex &write_output_mutex,
+ double paused_time_offset) {
for (;;) {
- av_packet.data = NULL;
- av_packet.size = 0;
- int res = avcodec_receive_packet(av_codec_context, &av_packet);
+ AVPacket *av_packet = av_packet_alloc();
+ if(!av_packet)
+ break;
+
+ av_packet->data = NULL;
+ av_packet->size = 0;
+ int res = avcodec_receive_packet(av_codec_context, av_packet);
if (res == 0) { // we have a packet, send the packet to the muxer
- av_packet.stream_index = stream_index;
- av_packet.pts = av_packet.dts = frame->pts;
+ av_packet->stream_index = stream_index;
+ av_packet->pts = pts;
+ av_packet->dts = pts;
- std::lock_guard<std::mutex> lock(write_output_mutex);
+ std::lock_guard<std::mutex> lock(write_output_mutex);
if(replay_buffer_size_secs != -1) {
- double time_now = glfwGetTime();
+ // TODO: Preallocate all frames data and use those instead.
+ // Why are we doing this you ask? there is a new ffmpeg bug that causes cpu usage to increase over time when you have
+ // packets that are not being free'd until later. So we copy the packet data, free the packet and then reconstruct
+ // the packet later on when we need it, to keep packets alive only for a short period.
+ auto new_packet = std::make_shared<PacketData>();
+ new_packet->data = *av_packet;
+ new_packet->data.data = (uint8_t*)av_malloc(av_packet->size);
+ memcpy(new_packet->data.data, av_packet->data, av_packet->size);
+
+ double time_now = clock_get_monotonic_seconds() - paused_time_offset;
double replay_time_elapsed = time_now - replay_start_time;
- AVPacket new_pack;
- av_packet_move_ref(&new_pack, &av_packet);
- frame_data_queue.push_back(std::move(new_pack));
+ frame_data_queue.push_back(std::move(new_packet));
if(replay_time_elapsed >= replay_buffer_size_secs) {
- av_packet_unref(&frame_data_queue.front());
frame_data_queue.pop_front();
frames_erased = true;
}
} else {
- av_packet_rescale_ts(&av_packet, av_codec_context->time_base, stream->time_base);
- av_packet.stream_index = stream->index;
- int ret = av_interleaved_write_frame(av_format_context, &av_packet);
+ av_packet_rescale_ts(av_packet, av_codec_context->time_base, stream->time_base);
+ av_packet->stream_index = stream->index;
+ // TODO: Is av_interleaved_write_frame needed?
+ int ret = av_write_frame(av_format_context, av_packet);
if(ret < 0) {
- fprintf(stderr, "Error: Failed to write frame index %d to muxer, reason: %s (%d)\n", av_packet.stream_index, av_error_to_string(ret), ret);
+ fprintf(stderr, "Error: Failed to write frame index %d to muxer, reason: %s (%d)\n", av_packet->stream_index, av_error_to_string(ret), ret);
}
}
- av_packet_unref(&av_packet);
+ av_packet_free(&av_packet);
} else if (res == AVERROR(EAGAIN)) { // we have no packet
// fprintf(stderr, "No packet!\n");
+ av_packet_free(&av_packet);
break;
} else if (res == AVERROR_EOF) { // this is the end of the stream
+ av_packet_free(&av_packet);
fprintf(stderr, "End of stream!\n");
break;
} else {
+ av_packet_free(&av_packet);
fprintf(stderr, "Unexpected error: %d\n", res);
break;
}
}
- //av_packet_unref(&av_packet);
}
-static AVCodecContext* create_audio_codec_context(AVFormatContext *av_format_context, int fps) {
- const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
+static const char* audio_codec_get_name(AudioCodec audio_codec) {
+ switch(audio_codec) {
+ case AudioCodec::AAC: return "aac";
+ case AudioCodec::OPUS: return "opus";
+ case AudioCodec::FLAC: return "flac";
+ }
+ assert(false);
+ return "";
+}
+
+static AVCodecID audio_codec_get_id(AudioCodec audio_codec) {
+ switch(audio_codec) {
+ case AudioCodec::AAC: return AV_CODEC_ID_AAC;
+ case AudioCodec::OPUS: return AV_CODEC_ID_OPUS;
+ case AudioCodec::FLAC: return AV_CODEC_ID_FLAC;
+ }
+ assert(false);
+ return AV_CODEC_ID_AAC;
+}
+
+static AVSampleFormat audio_codec_get_sample_format(AudioCodec audio_codec, const AVCodec *codec, bool mix_audio) {
+ switch(audio_codec) {
+ case AudioCodec::AAC: {
+ return AV_SAMPLE_FMT_FLTP;
+ }
+ case AudioCodec::OPUS: {
+ bool supports_s16 = false;
+ bool supports_flt = false;
+
+ for(size_t i = 0; codec->sample_fmts && codec->sample_fmts[i] != -1; ++i) {
+ if(codec->sample_fmts[i] == AV_SAMPLE_FMT_S16) {
+ supports_s16 = true;
+ } else if(codec->sample_fmts[i] == AV_SAMPLE_FMT_FLT) {
+ supports_flt = true;
+ }
+ }
+
+ // Amix only works with float audio
+ if(mix_audio)
+ supports_s16 = false;
+
+ if(!supports_s16 && !supports_flt) {
+ fprintf(stderr, "Warning: opus audio codec is chosen but your ffmpeg version does not support s16/flt sample format and performance might be slightly worse.\n");
+ fprintf(stderr, " You can either rebuild ffmpeg with libopus instead of the built-in opus, use the flatpak version of gpu screen recorder or record with aac audio codec instead (-ac aac).\n");
+ fprintf(stderr, " Falling back to fltp audio sample format instead.\n");
+ }
+
+ if(supports_s16)
+ return AV_SAMPLE_FMT_S16;
+ else if(supports_flt)
+ return AV_SAMPLE_FMT_FLT;
+ else
+ return AV_SAMPLE_FMT_FLTP;
+ }
+ case AudioCodec::FLAC: {
+ return AV_SAMPLE_FMT_S32;
+ }
+ }
+ assert(false);
+ return AV_SAMPLE_FMT_FLTP;
+}
+
+static int64_t audio_codec_get_get_bitrate(AudioCodec audio_codec) {
+ switch(audio_codec) {
+ case AudioCodec::AAC: return 160000;
+ case AudioCodec::OPUS: return 128000;
+ case AudioCodec::FLAC: return 128000;
+ }
+ assert(false);
+ return 128000;
+}
+
+static AudioFormat audio_codec_context_get_audio_format(const AVCodecContext *audio_codec_context) {
+ switch(audio_codec_context->sample_fmt) {
+ case AV_SAMPLE_FMT_FLT: return F32;
+ case AV_SAMPLE_FMT_FLTP: return S32;
+ case AV_SAMPLE_FMT_S16: return S16;
+ case AV_SAMPLE_FMT_S32: return S32;
+ default: return S16;
+ }
+}
+
+static AVSampleFormat audio_format_to_sample_format(const AudioFormat audio_format) {
+ switch(audio_format) {
+ case S16: return AV_SAMPLE_FMT_S16;
+ case S32: return AV_SAMPLE_FMT_S32;
+ case F32: return AV_SAMPLE_FMT_FLT;
+ }
+ assert(false);
+ return AV_SAMPLE_FMT_S16;
+}
+
+static AVCodecContext* create_audio_codec_context(int fps, AudioCodec audio_codec, bool mix_audio, int audio_bitrate) {
+ (void)fps;
+ const AVCodec *codec = avcodec_find_encoder(audio_codec_get_id(audio_codec));
if (!codec) {
- fprintf(
- stderr,
- "Error: Could not find aac encoder\n");
- exit(1);
+ fprintf(stderr, "Error: Could not find %s audio encoder\n", audio_codec_get_name(audio_codec));
+ _exit(1);
}
AVCodecContext *codec_context = avcodec_alloc_context3(codec);
assert(codec->type == AVMEDIA_TYPE_AUDIO);
- /*
- codec_context->sample_fmt = (*codec)->sample_fmts
- ? (*codec)->sample_fmts[0]
- : AV_SAMPLE_FMT_FLTP;
- */
- codec_context->codec_id = AV_CODEC_ID_AAC;
- codec_context->sample_fmt = AV_SAMPLE_FMT_FLTP;
- //codec_context->bit_rate = 64000;
+ codec_context->codec_id = codec->id;
+ codec_context->sample_fmt = audio_codec_get_sample_format(audio_codec, codec, mix_audio);
+ codec_context->bit_rate = audio_bitrate == 0 ? audio_codec_get_get_bitrate(audio_codec) : audio_bitrate;
codec_context->sample_rate = 48000;
+ if(audio_codec == AudioCodec::AAC)
+ codec_context->profile = FF_PROFILE_AAC_LOW;
+#if LIBAVCODEC_VERSION_MAJOR < 60
codec_context->channel_layout = AV_CH_LAYOUT_STEREO;
codec_context->channels = 2;
+#else
+ av_channel_layout_default(&codec_context->ch_layout, 2);
+#endif
codec_context->time_base.num = 1;
codec_context->time_base.den = AV_TIME_BASE;
- codec_context->framerate.num = fps;
- codec_context->framerate.den = 1;
-
- // Some formats want stream headers to be seperate
- if (av_format_context->oformat->flags & AVFMT_GLOBALHEADER)
- av_format_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+ codec_context->thread_count = 1;
+ codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
return codec_context;
}
-static AVCodecContext *create_video_codec_context(AVFormatContext *av_format_context,
+static AVCodecContext *create_video_codec_context(AVPixelFormat pix_fmt,
VideoQuality video_quality,
- int record_width, int record_height,
- int fps, bool use_hevc) {
- const AVCodec *codec = avcodec_find_encoder_by_name(use_hevc ? "hevc_nvenc" : "h264_nvenc");
- if (!codec) {
- codec = avcodec_find_encoder_by_name(use_hevc ? "nvenc_hevc" : "nvenc_h264");
- }
- if (!codec) {
- fprintf(
- stderr,
- "Error: Could not find %s encoder\n", use_hevc ? "hevc" : "h264");
- exit(1);
- }
+ int fps, const AVCodec *codec, bool is_livestream, gsr_gpu_vendor vendor, FramerateMode framerate_mode,
+ bool hdr, gsr_color_range color_range) {
AVCodecContext *codec_context = avcodec_alloc_context3(codec);
@@ -421,218 +334,583 @@ static AVCodecContext *create_video_codec_context(AVFormatContext *av_format_con
assert(codec->type == AVMEDIA_TYPE_VIDEO);
codec_context->codec_id = codec->id;
- codec_context->width = record_width & ~1;
- codec_context->height = record_height & ~1;
- codec_context->bit_rate = 12500000 + (codec_context->width * codec_context->height) / 2;
// Timebase: This is the fundamental unit of time (in seconds) in terms
// of which frame timestamps are represented. For fixed-fps content,
// timebase should be 1/framerate and timestamp increments should be
// identical to 1
codec_context->time_base.num = 1;
- codec_context->time_base.den = AV_TIME_BASE;
+ codec_context->time_base.den = framerate_mode == FramerateMode::CONSTANT ? fps : AV_TIME_BASE;
codec_context->framerate.num = fps;
codec_context->framerate.den = 1;
codec_context->sample_aspect_ratio.num = 0;
codec_context->sample_aspect_ratio.den = 0;
- codec_context->gop_size = fps * 2;
+ // High values reduce file size but increases time it takes to seek
+ if(is_livestream) {
+ codec_context->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY);
+ codec_context->flags2 |= AV_CODEC_FLAG2_FAST;
+ //codec_context->gop_size = std::numeric_limits<int>::max();
+ //codec_context->keyint_min = std::numeric_limits<int>::max();
+ codec_context->gop_size = fps * 2;
+ } else {
+ codec_context->gop_size = fps * 2;
+ }
codec_context->max_b_frames = 0;
- codec_context->pix_fmt = AV_PIX_FMT_CUDA;
- codec_context->color_range = AVCOL_RANGE_JPEG;
+ codec_context->pix_fmt = pix_fmt;
+ codec_context->color_range = color_range == GSR_COLOR_RANGE_LIMITED ? AVCOL_RANGE_MPEG : AVCOL_RANGE_JPEG;
+ if(hdr) {
+ codec_context->color_primaries = AVCOL_PRI_BT2020;
+ codec_context->color_trc = AVCOL_TRC_SMPTE2084;
+ codec_context->colorspace = AVCOL_SPC_BT2020_NCL;
+ } else {
+ codec_context->color_primaries = AVCOL_PRI_BT709;
+ codec_context->color_trc = AVCOL_TRC_BT709;
+ codec_context->colorspace = AVCOL_SPC_BT709;
+ }
+ //codec_context->chroma_sample_location = AVCHROMA_LOC_CENTER;
+ if(codec->id == AV_CODEC_ID_HEVC)
+ codec_context->codec_tag = MKTAG('h', 'v', 'c', '1');
switch(video_quality) {
case VideoQuality::MEDIUM:
- codec_context->bit_rate = 10000000 + (codec_context->width * codec_context->height) / 2;
- if(use_hevc) {
- codec_context->qmin = 20;
- codec_context->qmax = 35;
- } else {
- codec_context->qmin = 5;
- codec_context->qmax = 20;
- }
- //av_opt_set(codec_context->priv_data, "preset", "slow", 0);
- //av_opt_set(codec_context->priv_data, "profile", "high", 0);
- //codec_context->profile = FF_PROFILE_H264_HIGH;
- //av_opt_set(codec_context->priv_data, "preset", "p4", 0);
+ //codec_context->qmin = 35;
+ //codec_context->qmax = 35;
+ codec_context->bit_rate = 100000;//4500000 + (codec_context->width * codec_context->height)*0.75;
break;
case VideoQuality::HIGH:
- if(use_hevc) {
- codec_context->qmin = 17;
- codec_context->qmax = 30;
- } else {
- codec_context->qmin = 5;
- codec_context->qmax = 15;
- }
- //av_opt_set(codec_context->priv_data, "preset", "slow", 0);
- //av_opt_set(codec_context->priv_data, "profile", "high", 0);
- //codec_context->profile = FF_PROFILE_H264_HIGH;
- //av_opt_set(codec_context->priv_data, "preset", "p5", 0);
+ //codec_context->qmin = 34;
+ //codec_context->qmax = 34;
+ codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75;
+ break;
+ case VideoQuality::VERY_HIGH:
+ //codec_context->qmin = 28;
+ //codec_context->qmax = 28;
+ codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75;
break;
case VideoQuality::ULTRA:
- codec_context->bit_rate = 15000000 + (codec_context->width * codec_context->height) / 2;
- if(use_hevc) {
- codec_context->qmin = 16;
- codec_context->qmax = 25;
- } else {
- codec_context->qmin = 3;
- codec_context->qmax = 13;
- }
- //av_opt_set(codec_context->priv_data, "preset", "veryslow", 0);
- //av_opt_set(codec_context->priv_data, "profile", "high", 0);
- //codec_context->profile = FF_PROFILE_H264_HIGH;
- //av_opt_set(codec_context->priv_data, "preset", "p7", 0);
+ //codec_context->qmin = 22;
+ //codec_context->qmax = 22;
+ codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75;
break;
}
+ //codec_context->profile = FF_PROFILE_H264_MAIN;
if (codec_context->codec_id == AV_CODEC_ID_MPEG1VIDEO)
codec_context->mb_decision = 2;
// stream->time_base = codec_context->time_base;
// codec_context->ticks_per_frame = 30;
//av_opt_set(codec_context->priv_data, "tune", "hq", 0);
- //av_opt_set(codec_context->priv_data, "rc", "vbr", 0);
+ // TODO: Do this for better file size? also allows setting qmin, qmax per frame? which can then be used to dynamically set bitrate to reduce quality
+ // if live streaming is slow or if the users harddrive is cant handle writing megabytes of data per second.
+ #if 0
+ char qmin_str[32];
+ snprintf(qmin_str, sizeof(qmin_str), "%d", codec_context->qmin);
+
+ char qmax_str[32];
+ snprintf(qmax_str, sizeof(qmax_str), "%d", codec_context->qmax);
+
+ av_opt_set(codec_context->priv_data, "cq", qmax_str, 0);
+ av_opt_set(codec_context->priv_data, "rc", "vbr", 0);
+ av_opt_set(codec_context->priv_data, "qmin", qmin_str, 0);
+ av_opt_set(codec_context->priv_data, "qmax", qmax_str, 0);
+ codec_context->bit_rate = 0;
+ #endif
+
+ if(vendor != GSR_GPU_VENDOR_NVIDIA) {
+ switch(video_quality) {
+ case VideoQuality::MEDIUM:
+ codec_context->global_quality = 180;
+ break;
+ case VideoQuality::HIGH:
+ codec_context->global_quality = 140;
+ break;
+ case VideoQuality::VERY_HIGH:
+ codec_context->global_quality = 120;
+ break;
+ case VideoQuality::ULTRA:
+ codec_context->global_quality = 100;
+ break;
+ }
+ }
- // Some formats want stream headers to be seperate
- if (av_format_context->oformat->flags & AVFMT_GLOBALHEADER)
- av_format_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+ av_opt_set_int(codec_context->priv_data, "b_ref_mode", 0, 0);
+ //av_opt_set_int(codec_context->priv_data, "cbr", true, 0);
+
+ if(vendor != GSR_GPU_VENDOR_NVIDIA) {
+ // TODO: More options, better options
+ //codec_context->bit_rate = codec_context->width * codec_context->height;
+ av_opt_set(codec_context->priv_data, "rc_mode", "CQP", 0);
+ //codec_context->global_quality = 4;
+ //codec_context->compression_level = 2;
+ }
+
+ //av_opt_set(codec_context->priv_data, "bsf", "hevc_metadata=colour_primaries=9:transfer_characteristics=16:matrix_coefficients=9", 0);
+
+ //codec_context->rc_max_rate = codec_context->bit_rate;
+ //codec_context->rc_min_rate = codec_context->bit_rate;
+ //codec_context->rc_buffer_size = codec_context->bit_rate / 10;
+ // TODO: Do this when not using cqp
+ //codec_context->rc_initial_buffer_occupancy = codec_context->bit_rate * 1000;
+
+ codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
return codec_context;
}
-static AVFrame* open_audio(AVCodecContext *audio_codec_context) {
+static bool vaapi_create_codec_context(AVCodecContext *video_codec_context, const char *card_path) {
+ char render_path[128];
+ if(!gsr_card_path_get_render_path(card_path, render_path)) {
+ fprintf(stderr, "gsr error: failed to get /dev/dri/renderDXXX file from %s\n", card_path);
+ return false;
+ }
+
+ AVBufferRef *device_ctx;
+ if(av_hwdevice_ctx_create(&device_ctx, AV_HWDEVICE_TYPE_VAAPI, render_path, NULL, 0) < 0) {
+ fprintf(stderr, "Error: Failed to create hardware device context\n");
+ return false;
+ }
+
+ AVBufferRef *frame_context = av_hwframe_ctx_alloc(device_ctx);
+ if(!frame_context) {
+ fprintf(stderr, "Error: Failed to create hwframe context\n");
+ av_buffer_unref(&device_ctx);
+ return false;
+ }
+
+ AVHWFramesContext *hw_frame_context =
+ (AVHWFramesContext *)frame_context->data;
+ hw_frame_context->width = video_codec_context->width;
+ hw_frame_context->height = video_codec_context->height;
+ hw_frame_context->sw_format = AV_PIX_FMT_NV12;
+ hw_frame_context->format = video_codec_context->pix_fmt;
+ hw_frame_context->device_ref = device_ctx;
+ hw_frame_context->device_ctx = (AVHWDeviceContext*)device_ctx->data;
+
+ //hw_frame_context->initial_pool_size = 1;
+
+ if (av_hwframe_ctx_init(frame_context) < 0) {
+ fprintf(stderr, "Error: Failed to initialize hardware frame context "
+ "(note: ffmpeg version needs to be > 4.0)\n");
+ av_buffer_unref(&device_ctx);
+ //av_buffer_unref(&frame_context);
+ return false;
+ }
+
+ video_codec_context->hw_device_ctx = av_buffer_ref(device_ctx);
+ video_codec_context->hw_frames_ctx = av_buffer_ref(frame_context);
+ return true;
+}
+
+static bool check_if_codec_valid_for_hardware(const AVCodec *codec, gsr_gpu_vendor vendor, const char *card_path) {
+ // Do not use AV_PIX_FMT_CUDA because we dont want to do full check with hardware context
+ AVCodecContext *codec_context = create_video_codec_context(vendor == GSR_GPU_VENDOR_NVIDIA ? AV_PIX_FMT_YUV420P : AV_PIX_FMT_VAAPI, VideoQuality::VERY_HIGH, 60, codec, false, vendor, FramerateMode::CONSTANT, false, GSR_COLOR_RANGE_LIMITED);
+ if(!codec_context)
+ return false;
+
+ codec_context->width = 512;
+ codec_context->height = 512;
+
+ if(vendor != GSR_GPU_VENDOR_NVIDIA) {
+ if(!vaapi_create_codec_context(codec_context, card_path)) {
+ avcodec_free_context(&codec_context);
+ return false;
+ }
+ }
+
+ bool success = false;
+ success = avcodec_open2(codec_context, codec_context->codec, NULL) == 0;
+ if(codec_context->hw_device_ctx)
+ av_buffer_unref(&codec_context->hw_device_ctx);
+ if(codec_context->hw_frames_ctx)
+ av_buffer_unref(&codec_context->hw_frames_ctx);
+ avcodec_free_context(&codec_context);
+ return success;
+}
+
+static const AVCodec* find_h264_encoder(gsr_gpu_vendor vendor, const char *card_path) {
+ const AVCodec *codec = avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "h264_nvenc" : "h264_vaapi");
+ if(!codec)
+ codec = avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "nvenc_h264" : "vaapi_h264");
+
+ if(!codec)
+ return nullptr;
+
+ static bool checked = false;
+ static bool checked_success = true;
+ if(!checked) {
+ checked = true;
+ if(!check_if_codec_valid_for_hardware(codec, vendor, card_path))
+ checked_success = false;
+ }
+ return checked_success ? codec : nullptr;
+}
+
+static const AVCodec* find_h265_encoder(gsr_gpu_vendor vendor, const char *card_path) {
+ const AVCodec *codec = avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "hevc_nvenc" : "hevc_vaapi");
+ if(!codec)
+ codec = avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "nvenc_hevc" : "vaapi_hevc");
+
+ if(!codec)
+ return nullptr;
+
+ static bool checked = false;
+ static bool checked_success = true;
+ if(!checked) {
+ checked = true;
+ if(!check_if_codec_valid_for_hardware(codec, vendor, card_path))
+ checked_success = false;
+ }
+ return checked_success ? codec : nullptr;
+}
+
+static const AVCodec* find_av1_encoder(gsr_gpu_vendor vendor, const char *card_path) {
+ // Workaround bug with av1 nvidia in older ffmpeg versions that causes the whole application to crash
+ // when avcodec_open2 is opened with av1_nvenc
+ if(vendor == GSR_GPU_VENDOR_NVIDIA && LIBAVCODEC_BUILD < AV_VERSION_INT(60, 30, 100)) {
+ return nullptr;
+ }
+
+ const AVCodec *codec = avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "av1_nvenc" : "av1_vaapi");
+ if(!codec)
+ codec = avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "nvenc_av1" : "vaapi_av1");
+
+ if(!codec)
+ return nullptr;
+
+ static bool checked = false;
+ static bool checked_success = true;
+ if(!checked) {
+ checked = true;
+ if(!check_if_codec_valid_for_hardware(codec, vendor, card_path))
+ checked_success = false;
+ }
+ return checked_success ? codec : nullptr;
+}
+
+static void open_audio(AVCodecContext *audio_codec_context) {
+ AVDictionary *options = nullptr;
+ av_dict_set(&options, "strict", "experimental", 0);
+
int ret;
- ret = avcodec_open2(audio_codec_context, audio_codec_context->codec, nullptr);
+ ret = avcodec_open2(audio_codec_context, audio_codec_context->codec, &options);
if(ret < 0) {
fprintf(stderr, "failed to open codec, reason: %s\n", av_error_to_string(ret));
- exit(1);
+ _exit(1);
}
+}
+static AVFrame* create_audio_frame(AVCodecContext *audio_codec_context) {
AVFrame *frame = av_frame_alloc();
if(!frame) {
fprintf(stderr, "failed to allocate audio frame\n");
- exit(1);
+ _exit(1);
}
+ frame->sample_rate = audio_codec_context->sample_rate;
frame->nb_samples = audio_codec_context->frame_size;
frame->format = audio_codec_context->sample_fmt;
- frame->channels = audio_codec_context->channels;
+#if LIBAVCODEC_VERSION_MAJOR < 60
+ frame->channels = audio_codec_context->channels;
frame->channel_layout = audio_codec_context->channel_layout;
+#else
+ av_channel_layout_copy(&frame->ch_layout, &audio_codec_context->ch_layout);
+#endif
- ret = av_frame_get_buffer(frame, 0);
+ int ret = av_frame_get_buffer(frame, 0);
if(ret < 0) {
fprintf(stderr, "failed to allocate audio data buffers, reason: %s\n", av_error_to_string(ret));
- exit(1);
+ _exit(1);
}
return frame;
}
-static void open_video(AVCodecContext *codec_context,
- WindowPixmap &window_pixmap, AVBufferRef **device_ctx,
- CUgraphicsResource *cuda_graphics_resource, CUcontext cuda_context) {
- int ret;
+static void open_video(AVCodecContext *codec_context, VideoQuality video_quality, bool very_old_gpu, gsr_gpu_vendor vendor, PixelFormat pixel_format, bool hdr) {
+ AVDictionary *options = nullptr;
+ if(vendor == GSR_GPU_VENDOR_NVIDIA) {
+ bool supports_p4 = false;
+ bool supports_p5 = false;
+
+ const AVOption *opt = nullptr;
+ while((opt = av_opt_next(codec_context->priv_data, opt))) {
+ if(opt->type == AV_OPT_TYPE_CONST) {
+ if(strcmp(opt->name, "p4") == 0)
+ supports_p4 = true;
+ else if(strcmp(opt->name, "p5") == 0)
+ supports_p5 = true;
+ }
+ }
- *device_ctx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA);
- if(!*device_ctx) {
- fprintf(stderr, "Error: Failed to create hardware device context\n");
- exit(1);
- }
+ if(codec_context->codec_id == AV_CODEC_ID_AV1) {
+ switch(video_quality) {
+ case VideoQuality::MEDIUM:
+ av_dict_set_int(&options, "qp", 37, 0);
+ break;
+ case VideoQuality::HIGH:
+ av_dict_set_int(&options, "qp", 32, 0);
+ break;
+ case VideoQuality::VERY_HIGH:
+ av_dict_set_int(&options, "qp", 28, 0);
+ break;
+ case VideoQuality::ULTRA:
+ av_dict_set_int(&options, "qp", 24, 0);
+ break;
+ }
+ } else if(very_old_gpu || codec_context->codec_id == AV_CODEC_ID_H264) {
+ switch(video_quality) {
+ case VideoQuality::MEDIUM:
+ av_dict_set_int(&options, "qp", 37, 0);
+ break;
+ case VideoQuality::HIGH:
+ av_dict_set_int(&options, "qp", 32, 0);
+ break;
+ case VideoQuality::VERY_HIGH:
+ av_dict_set_int(&options, "qp", 27, 0);
+ break;
+ case VideoQuality::ULTRA:
+ av_dict_set_int(&options, "qp", 21, 0);
+ break;
+ }
+ } else {
+ switch(video_quality) {
+ case VideoQuality::MEDIUM:
+ av_dict_set_int(&options, "qp", 37, 0);
+ break;
+ case VideoQuality::HIGH:
+ av_dict_set_int(&options, "qp", 32, 0);
+ break;
+ case VideoQuality::VERY_HIGH:
+ av_dict_set_int(&options, "qp", 28, 0);
+ break;
+ case VideoQuality::ULTRA:
+ av_dict_set_int(&options, "qp", 24, 0);
+ break;
+ }
+ }
- AVHWDeviceContext *hw_device_context = (AVHWDeviceContext *)(*device_ctx)->data;
- AVCUDADeviceContext *cuda_device_context = (AVCUDADeviceContext *)hw_device_context->hwctx;
- cuda_device_context->cuda_ctx = cuda_context;
- if(av_hwdevice_ctx_init(*device_ctx) < 0) {
- fprintf(stderr, "Error: Failed to create hardware device context\n");
- exit(1);
- }
+ if(!supports_p4 && !supports_p5)
+ fprintf(stderr, "Info: your ffmpeg version is outdated. It's recommended that you use the flatpak version of gpu-screen-recorder version instead, which you can find at https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder\n");
+
+ //if(is_livestream) {
+ // av_dict_set_int(&options, "zerolatency", 1, 0);
+ // //av_dict_set(&options, "preset", "llhq", 0);
+ //}
+
+ // I want to use a good preset for the gpu but all gpus prefer different
+ // presets. Nvidia and ffmpeg used to support "hq" preset that chose the best preset for the gpu
+ // with pretty good performance but you now have to choose p1-p7, which are gpu agnostic and on
+ // older gpus p5-p7 slow the gpu down to a crawl...
+ // "hq" is now just an alias for p7 in ffmpeg :(
+ // TODO: Temporary disable because of stuttering?
+
+ // TODO: Preset is set to p5 for now but it should ideally be p6 or p7.
+ // This change is needed because for certain sizes of a window (or monitor?) such as 971x780 causes encoding to freeze
+ // when using h264 codec. This is a new(?) nvidia driver bug.
+ if(very_old_gpu)
+ av_dict_set(&options, "preset", supports_p4 ? "p4" : "medium", 0);
+ else
+ av_dict_set(&options, "preset", supports_p5 ? "p5" : "slow", 0);
+
+ av_dict_set(&options, "tune", "hq", 0);
+ av_dict_set(&options, "rc", "constqp", 0);
+
+ if(codec_context->codec_id == AV_CODEC_ID_H264) {
+ switch(pixel_format) {
+ case PixelFormat::YUV420:
+ av_dict_set(&options, "profile", "high", 0);
+ break;
+ case PixelFormat::YUV444:
+ av_dict_set(&options, "profile", "high444p", 0);
+ break;
+ }
+ } else if(codec_context->codec_id == AV_CODEC_ID_AV1) {
+ switch(pixel_format) {
+ case PixelFormat::YUV420:
+ av_dict_set(&options, "rgb_mode", "yuv420", 0);
+ break;
+ case PixelFormat::YUV444:
+ av_dict_set(&options, "rgb_mode", "yuv444", 0);
+ break;
+ }
+ } else {
+ //av_dict_set(&options, "profile", "main10", 0);
+ //av_dict_set(&options, "pix_fmt", "yuv420p16le", 0);
+ if(hdr) {
+ av_dict_set(&options, "profile", "main10", 0);
+ } else {
+ av_dict_set(&options, "profile", "main", 0);
+ }
+ }
+ } else {
+ if(codec_context->codec_id == AV_CODEC_ID_AV1) {
+ // Using global_quality option
+ } else if(codec_context->codec_id == AV_CODEC_ID_H264) {
+ switch(video_quality) {
+ case VideoQuality::MEDIUM:
+ av_dict_set_int(&options, "qp", 34, 0);
+ break;
+ case VideoQuality::HIGH:
+ av_dict_set_int(&options, "qp", 30, 0);
+ break;
+ case VideoQuality::VERY_HIGH:
+ av_dict_set_int(&options, "qp", 26, 0);
+ break;
+ case VideoQuality::ULTRA:
+ av_dict_set_int(&options, "qp", 22, 0);
+ break;
+ }
+ } else {
+ switch(video_quality) {
+ case VideoQuality::MEDIUM:
+ av_dict_set_int(&options, "qp", 37, 0);
+ break;
+ case VideoQuality::HIGH:
+ av_dict_set_int(&options, "qp", 32, 0);
+ break;
+ case VideoQuality::VERY_HIGH:
+ av_dict_set_int(&options, "qp", 28, 0);
+ break;
+ case VideoQuality::ULTRA:
+ av_dict_set_int(&options, "qp", 24, 0);
+ break;
+ }
+ }
- AVBufferRef *frame_context = av_hwframe_ctx_alloc(*device_ctx);
- if (!frame_context) {
- fprintf(stderr, "Error: Failed to create hwframe context\n");
- exit(1);
- }
+ // TODO: More quality options
+ av_dict_set(&options, "rc_mode", "CQP", 0);
+ //av_dict_set_int(&options, "low_power", 1, 0);
- AVHWFramesContext *hw_frame_context =
- (AVHWFramesContext *)frame_context->data;
- hw_frame_context->width = codec_context->width;
- hw_frame_context->height = codec_context->height;
- hw_frame_context->sw_format = AV_PIX_FMT_0RGB32;
- hw_frame_context->format = codec_context->pix_fmt;
- hw_frame_context->device_ref = *device_ctx;
- hw_frame_context->device_ctx = (AVHWDeviceContext *)(*device_ctx)->data;
+ if(codec_context->codec_id == AV_CODEC_ID_H264) {
+ av_dict_set(&options, "profile", "high", 0);
+ //av_dict_set_int(&options, "quality", 5, 0); // quality preset
+ } else if(codec_context->codec_id == AV_CODEC_ID_AV1) {
+ av_dict_set(&options, "profile", "main", 0); // TODO: use professional instead?
+ av_dict_set(&options, "tier", "main", 0);
+ } else {
+ if(hdr) {
+ av_dict_set(&options, "profile", "main10", 0);
+ av_dict_set(&options, "sei", "hdr", 0);
+ } else {
+ av_dict_set(&options, "profile", "main", 0);
+ }
+ }
+ }
- if (av_hwframe_ctx_init(frame_context) < 0) {
- fprintf(stderr, "Error: Failed to initialize hardware frame context "
- "(note: ffmpeg version needs to be > 4.0\n");
- exit(1);
+ if(codec_context->codec_id == AV_CODEC_ID_H264) {
+ av_dict_set(&options, "coder", "cabac", 0); // TODO: cavlc is faster than cabac but worse compression. Which to use?
}
- codec_context->hw_device_ctx = *device_ctx;
- codec_context->hw_frames_ctx = frame_context;
+ av_dict_set(&options, "strict", "experimental", 0);
- ret = avcodec_open2(codec_context, codec_context->codec, nullptr);
+ int ret = avcodec_open2(codec_context, codec_context->codec, &options);
if (ret < 0) {
- fprintf(stderr, "Error: Could not open video codec: %s\n",
- "blabla"); // av_err2str(ret));
- exit(1);
- }
-
- if(window_pixmap.target_texture_id != 0) {
- CUresult res;
- CUcontext old_ctx;
- res = cuCtxPopCurrent(&old_ctx);
- res = cuCtxPushCurrent(cuda_context);
- res = cuGraphicsGLRegisterImage(
- cuda_graphics_resource, window_pixmap.target_texture_id, GL_TEXTURE_2D,
- CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY);
- // cuGraphicsUnregisterResource(*cuda_graphics_resource);
- if (res != CUDA_SUCCESS) {
- const char *err_str;
- cuGetErrorString(res, &err_str);
- fprintf(stderr,
- "Error: cuGraphicsGLRegisterImage failed, error %s, texture "
- "id: %u\n",
- err_str, window_pixmap.target_texture_id);
- exit(1);
- }
- res = cuCtxPopCurrent(&old_ctx);
+ fprintf(stderr, "Error: Could not open video codec: %s\n", av_error_to_string(ret));
+ _exit(1);
}
}
-static void close_video(AVStream *video_stream, AVFrame *frame) {
- // avcodec_close(video_stream->codec);
- // av_frame_free(&frame);
+static void usage_header() {
+ const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
+ const char *program_name = inside_flatpak ? "flatpak run --command=gpu-screen-recorder com.dec05eba.gpu_screen_recorder" : "gpu-screen-recorder";
+ fprintf(stderr, "usage: %s -w <window_id|monitor|focused> [-c <container_format>] [-s WxH] -f <fps> [-a <audio_input>] [-q <quality>] [-r <replay_buffer_size_sec>] [-k h264|hevc|hevc_hdr|av1|av1_hdr] [-ac aac|opus|flac] [-ab <bitrate>] [-oc yes|no] [-fm cfr|vfr] [-cr limited|full] [-v yes|no] [-h|--help] [-o <output_file>] [-mf yes|no] [-sc <script_path>] [-cursor yes|no]\n", program_name);
}
-static void usage() {
- fprintf(stderr, "usage: gpu-screen-recorder -w <window_id> -c <container_format> -f <fps> [-a <audio_input>] [-q <quality>] [-r <replay_buffer_size_sec>] [-o <output_file>]\n");
+static void usage_full() {
+ const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
+ const char *program_name = inside_flatpak ? "flatpak run --command=gpu-screen-recorder com.dec05eba.gpu_screen_recorder" : "gpu-screen-recorder";
+ usage_header();
+ fprintf(stderr, "\n");
fprintf(stderr, "OPTIONS:\n");
- fprintf(stderr, " -w Window to record or a display, \"screen\" or \"screen-direct\". The display is the display name in xrandr and if \"screen\" or \"screen-direct\" is selected then all displays are recorded and they are recorded in h265 (aka hevc)."
- "\"screen-direct\" skips one texture copy for fullscreen applications so it may lead to better performance and it works with VRR monitors when recording fullscreen application but may break some applications, such as mpv in fullscreen mode. Recording a display requires a gpu with NvFBC support.\n");
- fprintf(stderr, " -s The size (area) to record at in the format WxH, for example 1920x1080. Usually you want to set this to the size of the window. Optional, by default the size of the window, monitor or screen is used (which is passed to -w).\n");
- fprintf(stderr, " -c Container format for output file, for example mp4, or flv.\n");
- fprintf(stderr, " -f Framerate to record at. Clamped to [1,250].\n");
- fprintf(stderr, " -a Audio device to record from (pulse audio device). Optional, disabled by default.\n");
- fprintf(stderr, " -q Video quality. Should either be 'medium', 'high' or 'ultra'. Optional, set to 'medium' be default.\n");
- fprintf(stderr, " -r Replay buffer size in seconds. If this is set, then only the last seconds as set by this option will be stored"
- " and the video will only be saved when the gpu-screen-recorder is closed. This feature is similar to Nvidia's instant replay feature."
- " This option has be between 5 and 1200. Note that the replay buffer size will not always be precise, because of keyframes. Optional, disabled by default.\n");
- fprintf(stderr, " -o The output file path. If omitted then the encoded data is sent to stdout. Required in replay mode (when using -r). In replay mode this has to be an existing directory instead of a file.\n");
+ fprintf(stderr, " -w Window id to record, a display (monitor name), \"screen\", \"screen-direct-force\" or \"focused\".\n");
+ fprintf(stderr, " If this is \"screen\" or \"screen-direct-force\" then all monitors are recorded.\n");
+ fprintf(stderr, " \"screen-direct-force\" is not recommended unless you use a VRR monitor on Nvidia X11 and you are aware that using this option can cause games to freeze/crash or other issues because of Nvidia driver issues.\n");
+ fprintf(stderr, " \"screen-direct-force\" option is only available on Nvidia X11. VRR works without this option on other systems.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -c Container format for output file, for example mp4, or flv. Only required if no output file is specified or if recording in replay buffer mode.\n");
+ fprintf(stderr, " If an output file is specified and -c is not used then the container format is determined from the output filename extension.\n");
+ fprintf(stderr, " Only containers that support h264, hevc or av1 are supported, which means that only mp4, mkv, flv (and some others) are supported.\n");
+ fprintf(stderr, " WebM is not supported yet (most hardware doesn't support WebM video encoding).\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -s The size (area) to record at in the format WxH, for example 1920x1080. This option is only supported (and required) when -w is \"focused\".\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -f Framerate to record at.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -a Audio device to record from (pulse audio device). Can be specified multiple times. Each time this is specified a new audio track is added for the specified audio device.\n");
+ fprintf(stderr, " A name can be given to the audio input device by prefixing the audio input with <name>/, for example \"dummy/alsa_output.pci-0000_00_1b.0.analog-stereo.monitor\".\n");
+ fprintf(stderr, " Multiple audio devices can be merged into one audio track by using \"|\" as a separator into one -a argument, for example: -a \"alsa_output1|alsa_output2\".\n");
+ fprintf(stderr, " Optional, no audio track is added by default.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -q Video quality. Should be either 'medium', 'high', 'very_high' or 'ultra'. 'high' is the recommended option when live streaming or when you have a slower harddrive.\n");
+ fprintf(stderr, " Optional, set to 'very_high' be default.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -r Replay buffer size in seconds. If this is set, then only the last seconds as set by this option will be stored\n");
+ fprintf(stderr, " and the video will only be saved when the gpu-screen-recorder is closed. This feature is similar to Nvidia's instant replay feature.\n");
+ fprintf(stderr, " This option has be between 5 and 1200. Note that the replay buffer size will not always be precise, because of keyframes. Optional, disabled by default.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -k Video codec to use. Should be either 'auto', 'h264', 'hevc', 'av1', 'hevc_hdr' or 'av1_hdr'. Defaults to 'auto' which defaults to 'hevc' on AMD/Nvidia and 'h264' on intel.\n");
+ fprintf(stderr, " Forcefully set to 'h264' if the file container type is 'flv'.\n");
+ fprintf(stderr, " Forcefully set to 'hevc' on AMD/intel if video codec is 'h264' and if the file container type is 'mkv'.\n");
+ fprintf(stderr, " 'hevc_hdr' and 'av1_hdr' option is not available on X11.\n");
+ fprintf(stderr, " Note: hdr metadata is not included in the video when recording with 'hevc_hdr'/'av1_hdr' because of bugs in AMD, Intel and NVIDIA drivers (amazin', they are bugged).\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -ac Audio codec to use. Should be either 'aac', 'opus' or 'flac'. Defaults to 'opus' for .mp4/.mkv files, otherwise defaults to 'aac'.\n");
+ fprintf(stderr, " 'opus' and 'flac' is only supported by .mp4/.mkv files. 'opus' is recommended for best performance and smallest audio size.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -ab Audio bitrate to use. Optional, by default the bitrate is 128000 for opus and flac and 160000 for aac.\n");
+ fprintf(stderr, " If this is set to 0 then it's the same as if it's absent, in which case the bitrate is determined automatically depending on the audio codec.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -oc Overclock memory transfer rate to the maximum performance level. This only applies to NVIDIA on X11 and exists to overcome a bug in NVIDIA driver where performance level\n");
+ fprintf(stderr, " is dropped when you record a game. Only needed if you are recording a game that is bottlenecked by GPU. The same issue exists on Wayland but overclocking is not possible on Wayland.\n");
+ fprintf(stderr, " Works only if your have \"Coolbits\" set to \"12\" in NVIDIA X settings, see README for more information. Note! use at your own risk! Optional, disabled by default.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -fm Framerate mode. Should be either 'cfr' or 'vfr'. Defaults to 'vfr'.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -cr Color range. Should be either 'limited' (aka mpeg) or 'full' (aka jpeg). Defaults to 'limited'.\n");
+ fprintf(stderr, " Limited color range means that colors are in range 16-235 while full color range means that colors are in range 0-255 (when not recording with hdr).\n");
+ fprintf(stderr, " Note that some buggy video players (such as vlc) are unable to correctly display videos in full color range.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -v Prints per second, fps updates. Optional, set to 'yes' by default.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -h, --help\n");
+ fprintf(stderr, " Show this help.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -mf Organise replays in folders based on the current date.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -sc Run a script on the saved video file (non-blocking). The first argument to the script is the filepath to the saved video file and the second argument is the recording type (either \"regular\" or \"replay\").\n");
+ fprintf(stderr, " Not applicable for live streams.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -cursor\n");
+ fprintf(stderr, " Record cursor. Defaults to 'yes'.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " --list-supported-video-codecs\n");
+ fprintf(stderr, " List supported video codecs and exits. Prints h264, hevc, hevc_hdr, av1 and av1_hdr (if supported).\n");
+ fprintf(stderr, "\n");
+ //fprintf(stderr, " -pixfmt The pixel format to use for the output video. yuv420 is the most common format and is best supported, but the color is compressed, so colors can look washed out and certain colors of text can look bad. Use yuv444 for no color compression, but the video may not work everywhere and it may not work with hardware video decoding. Optional, defaults to yuv420\n");
+ fprintf(stderr, " -o The output file path. If omitted then the encoded data is sent to stdout. Required in replay mode (when using -r).\n");
+ fprintf(stderr, " In replay mode this has to be a directory instead of a file.\n");
+ fprintf(stderr, " The directory to the file is created (recursively) if it doesn't already exist.\n");
+ fprintf(stderr, "\n");
fprintf(stderr, "NOTES:\n");
- fprintf(stderr, " Send signal SIGINT (Ctrl+C) to gpu-screen-recorder to stop and save the recording (when not using replay mode).\n");
- fprintf(stderr, " Send signal SIGUSR1 (killall -SIGUSR1 gpu-screen-recorder) to gpu-screen-recorder to save a replay.\n");
- exit(1);
+ fprintf(stderr, " Send signal SIGINT to gpu-screen-recorder (Ctrl+C, or killall -SIGINT gpu-screen-recorder) to stop and save the recording. When in replay mode this stops recording without saving.\n");
+ fprintf(stderr, " Send signal SIGUSR1 to gpu-screen-recorder (killall -SIGUSR1 gpu-screen-recorder) to save a replay (when in replay mode).\n");
+ fprintf(stderr, " Send signal SIGUSR2 to gpu-screen-recorder (killall -SIGUSR2 gpu-screen-recorder) to pause/unpause recording. Only applicable and useful when recording (not streaming nor replay).\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, "EXAMPLES:\n");
+ fprintf(stderr, " %s -w screen -f 60 -a \"$(pactl get-default-sink).monitor\" -o \"$HOME/Videos/video.mp4\"\n", program_name);
+ fprintf(stderr, " %s -w screen -f 60 -a \"$(pactl get-default-sink).monitor|$(pactl get-default-source)\" -o \"$HOME/Videos/video.mp4\"\n", program_name);
+ fprintf(stderr, " %s -w screen -f 60 -a \"$(pactl get-default-sink).monitor\" -c mkv -r 60 -o \"$HOME/Videos\"\n", program_name);
+ //fprintf(stderr, " gpu-screen-recorder -w screen -f 60 -q ultra -pixfmt yuv444 -o video.mp4\n");
+ _exit(1);
+}
+
+static void usage() {
+ usage_header();
+ _exit(1);
}
-static sig_atomic_t started = 0;
static sig_atomic_t running = 1;
static sig_atomic_t save_replay = 0;
-static const char *pid_file = "/tmp/gpu-screen-recorder";
+static sig_atomic_t toggle_pause = 0;
-static void term_handler(int) {
- if(started)
- unlink(pid_file);
- exit(0);
-}
-
-static void int_handler(int) {
+static void stop_handler(int) {
running = 0;
}
@@ -640,30 +918,35 @@ static void save_replay_handler(int) {
save_replay = 1;
}
-struct Arg {
- const char *value;
- bool optional;
-};
+static void toggle_pause_handler(int) {
+ toggle_pause = 1;
+}
static bool is_hex_num(char c) {
return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9');
}
static bool contains_non_hex_number(const char *str) {
+ bool hex_start = false;
size_t len = strlen(str);
if(len >= 2 && memcmp(str, "0x", 2) == 0) {
str += 2;
len -= 2;
+ hex_start = true;
}
+ bool is_hex = false;
for(size_t i = 0; i < len; ++i) {
char c = str[i];
if(c == '\0')
return false;
if(!is_hex_num(c))
return true;
+ if((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))
+ is_hex = true;
}
- return false;
+
+ return is_hex && !hex_start;
}
static std::string get_date_str() {
@@ -674,11 +957,27 @@ static std::string get_date_str() {
return str;
}
+static std::string get_date_only_str() {
+ char str[128];
+ time_t now = time(NULL);
+ struct tm *t = localtime(&now);
+ strftime(str, sizeof(str)-1, "%Y-%m-%d", t);
+ return str;
+}
+
+static std::string get_time_only_str() {
+ char str[128];
+ time_t now = time(NULL);
+ struct tm *t = localtime(&now);
+ strftime(str, sizeof(str)-1, "%H-%M-%S", t);
+ return str;
+}
+
static AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContext *codec_context) {
AVStream *stream = avformat_new_stream(av_format_context, nullptr);
if (!stream) {
fprintf(stderr, "Error: Could not allocate stream\n");
- exit(1);
+ _exit(1);
}
stream->id = av_format_context->nb_streams - 1;
stream->time_base = codec_context->time_base;
@@ -686,51 +985,174 @@ static AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContex
return stream;
}
+static void run_recording_saved_script_async(const char *script_file, const char *video_file, const char *type) {
+ char script_file_full[PATH_MAX];
+ script_file_full[0] = '\0';
+ if(!realpath(script_file, script_file_full)) {
+ fprintf(stderr, "Error: script file not found: %s\n", script_file);
+ return;
+ }
+
+ const char *args[6];
+ const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
+
+ if(inside_flatpak) {
+ args[0] = "flatpak-spawn";
+ args[1] = "--host";
+ args[2] = script_file_full;
+ args[3] = video_file;
+ args[4] = type;
+ args[5] = NULL;
+ } else {
+ args[0] = script_file_full;
+ args[1] = video_file;
+ args[2] = type;
+ args[3] = NULL;
+ }
+
+ pid_t pid = fork();
+ if(pid == -1) {
+ perror(script_file_full);
+ return;
+ } else if(pid == 0) { // child
+ setsid();
+ signal(SIGHUP, SIG_IGN);
+
+ pid_t second_child = fork();
+ if(second_child == 0) { // child
+ execvp(args[0], (char* const*)args);
+ perror(script_file_full);
+ _exit(127);
+ } else if(second_child != -1) { // parent
+ _exit(0);
+ }
+ } else { // parent
+ waitpid(pid, NULL, 0);
+ }
+}
+
+struct AudioDevice {
+ SoundDevice sound_device;
+ AudioInput audio_input;
+ AVFilterContext *src_filter_ctx = nullptr;
+ AVFrame *frame = nullptr;
+ std::thread thread; // TODO: Instead of having a thread for each track, have one thread for all threads and read the data with non-blocking read
+};
+
+// TODO: Cleanup
+struct AudioTrack {
+ AVCodecContext *codec_context = nullptr;
+ AVStream *stream = nullptr;
+
+ std::vector<AudioDevice> audio_devices;
+ AVFilterGraph *graph = nullptr;
+ AVFilterContext *sink = nullptr;
+ int stream_index = 0;
+ int64_t pts = 0;
+};
+
static std::future<void> save_replay_thread;
-static std::vector<AVPacket> save_replay_packets;
+static std::vector<std::shared_ptr<PacketData>> save_replay_packets;
static std::string save_replay_output_filepath;
-static void save_replay_async(AVCodecContext *video_codec_context, AVCodecContext *audio_codec_context, int video_stream_index, int audio_stream_index, const std::deque<AVPacket> &frame_data_queue, bool frames_erased, std::string output_dir, std::string container_format) {
+static int create_directory_recursive(char *path) {
+ int path_len = strlen(path);
+ char *p = path;
+ char *end = path + path_len;
+ for(;;) {
+ char *slash_p = strchr(p, '/');
+
+ // Skips first '/', we don't want to try and create the root directory
+ if(slash_p == path) {
+ ++p;
+ continue;
+ }
+
+ if(!slash_p)
+ slash_p = end;
+
+ char prev_char = *slash_p;
+ *slash_p = '\0';
+ int err = mkdir(path, S_IRWXU);
+ *slash_p = prev_char;
+
+ if(err == -1 && errno != EEXIST)
+ return err;
+
+ if(slash_p == end)
+ break;
+ else
+ p = slash_p + 1;
+ }
+ return 0;
+}
+
+static void save_replay_async(AVCodecContext *video_codec_context, int video_stream_index, std::vector<AudioTrack> &audio_tracks, std::deque<std::shared_ptr<PacketData>> &frame_data_queue, bool frames_erased, std::string output_dir, const char *container_format, const std::string &file_extension, std::mutex &write_output_mutex, bool make_folders) {
if(save_replay_thread.valid())
return;
size_t start_index = (size_t)-1;
- for(size_t i = 0; i < frame_data_queue.size(); ++i) {
- const AVPacket &av_packet = frame_data_queue[i];
- if((av_packet.flags & AV_PKT_FLAG_KEY) && av_packet.stream_index == video_stream_index) {
- start_index = i;
- break;
+ int64_t video_pts_offset = 0;
+ int64_t audio_pts_offset = 0;
+
+ {
+ std::lock_guard<std::mutex> lock(write_output_mutex);
+ start_index = (size_t)-1;
+ for(size_t i = 0; i < frame_data_queue.size(); ++i) {
+ const AVPacket &av_packet = frame_data_queue[i]->data;
+ if((av_packet.flags & AV_PKT_FLAG_KEY) && av_packet.stream_index == video_stream_index) {
+ start_index = i;
+ break;
+ }
}
- }
- if(start_index == (size_t)-1)
- return;
+ if(start_index == (size_t)-1)
+ return;
- int64_t pts_offset = 0;
- if(frames_erased)
- pts_offset = frame_data_queue[start_index].pts;
+ if(frames_erased) {
+ video_pts_offset = frame_data_queue[start_index]->data.pts;
+
+ // Find the next audio packet to use as audio pts offset
+ for(size_t i = start_index; i < frame_data_queue.size(); ++i) {
+ const AVPacket &av_packet = frame_data_queue[i]->data;
+ if(av_packet.stream_index != video_stream_index) {
+ audio_pts_offset = av_packet.pts;
+ break;
+ }
+ }
+ } else {
+ start_index = 0;
+ }
- save_replay_packets.resize(frame_data_queue.size());
- for(size_t i = 0; i < frame_data_queue.size(); ++i) {
- av_packet_ref(&save_replay_packets[i], &frame_data_queue[i]);
+ save_replay_packets.resize(frame_data_queue.size());
+ for(size_t i = 0; i < frame_data_queue.size(); ++i) {
+ save_replay_packets[i] = frame_data_queue[i];
+ }
}
- save_replay_output_filepath = output_dir + "/Replay_" + get_date_str() + "." + container_format;
- save_replay_thread = std::async(std::launch::async, [video_stream_index, container_format, start_index, pts_offset, video_codec_context, audio_codec_context]() mutable {
- AVFormatContext *av_format_context;
- // The output format is automatically guessed from the file extension
- avformat_alloc_output_context2(&av_format_context, nullptr, container_format.c_str(), nullptr);
+ if (make_folders) {
+ std::string output_folder = output_dir + '/' + get_date_only_str();
+ create_directory_recursive(&output_folder[0]);
+ save_replay_output_filepath = output_folder + "/Replay_" + get_time_only_str() + "." + file_extension;
+ } else {
+ create_directory_recursive(&output_dir[0]);
+ save_replay_output_filepath = output_dir + "/Replay_" + get_date_str() + "." + file_extension;
+ }
- av_format_context->flags |= AVFMT_FLAG_GENPTS;
- if (av_format_context->oformat->flags & AVFMT_GLOBALHEADER)
- av_format_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+ save_replay_thread = std::async(std::launch::async, [video_stream_index, container_format, start_index, video_pts_offset, audio_pts_offset, video_codec_context, &audio_tracks]() mutable {
+ AVFormatContext *av_format_context;
+ avformat_alloc_output_context2(&av_format_context, nullptr, container_format, nullptr);
AVStream *video_stream = create_stream(av_format_context, video_codec_context);
- AVStream *audio_stream = audio_codec_context ? create_stream(av_format_context, audio_codec_context) : nullptr;
-
avcodec_parameters_from_context(video_stream->codecpar, video_codec_context);
- if(audio_stream)
- avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_context);
+
+ std::unordered_map<int, AudioTrack*> stream_index_to_audio_track_map;
+ for(AudioTrack &audio_track : audio_tracks) {
+ stream_index_to_audio_track_map[audio_track.stream_index] = &audio_track;
+ AVStream *audio_stream = create_stream(av_format_context, audio_track.codec_context);
+ avcodec_parameters_from_context(audio_stream->codecpar, audio_track.codec_context);
+ audio_track.stream = audio_stream;
+ }
int ret = avio_open(&av_format_context->pb, save_replay_output_filepath.c_str(), AVIO_FLAG_WRITE);
if (ret < 0) {
@@ -738,26 +1160,50 @@ static void save_replay_async(AVCodecContext *video_codec_context, AVCodecContex
return;
}
- ret = avformat_write_header(av_format_context, nullptr);
+ AVDictionary *options = nullptr;
+ av_dict_set(&options, "strict", "experimental", 0);
+
+ ret = avformat_write_header(av_format_context, &options);
if (ret < 0) {
fprintf(stderr, "Error occurred when writing header to output file: %s\n", av_error_to_string(ret));
return;
}
for(size_t i = start_index; i < save_replay_packets.size(); ++i) {
- AVPacket &av_packet = save_replay_packets[i];
+ // TODO: Check if successful
+ AVPacket av_packet;
+ memset(&av_packet, 0, sizeof(av_packet));
+ //av_packet_from_data(av_packet, save_replay_packets[i]->data.data, save_replay_packets[i]->data.size);
+ av_packet.data = save_replay_packets[i]->data.data;
+ av_packet.size = save_replay_packets[i]->data.size;
+ av_packet.stream_index = save_replay_packets[i]->data.stream_index;
+ av_packet.pts = save_replay_packets[i]->data.pts;
+ av_packet.dts = save_replay_packets[i]->data.pts;
+ av_packet.flags = save_replay_packets[i]->data.flags;
+
+ AVStream *stream = video_stream;
+ AVCodecContext *codec_context = video_codec_context;
+
+ if(av_packet.stream_index == video_stream_index) {
+ av_packet.pts -= video_pts_offset;
+ av_packet.dts -= video_pts_offset;
+ } else {
+ AudioTrack *audio_track = stream_index_to_audio_track_map[av_packet.stream_index];
+ stream = audio_track->stream;
+ codec_context = audio_track->codec_context;
- AVStream *stream = av_packet.stream_index == video_stream_index ? video_stream : audio_stream;
- AVCodecContext *codec_context = av_packet.stream_index == video_stream_index ? video_codec_context : audio_codec_context;
+ av_packet.pts -= audio_pts_offset;
+ av_packet.dts -= audio_pts_offset;
+ }
av_packet.stream_index = stream->index;
- av_packet.pts -= pts_offset;
- av_packet.dts -= pts_offset;
av_packet_rescale_ts(&av_packet, codec_context->time_base, stream->time_base);
- int ret = av_interleaved_write_frame(av_format_context, &av_packet);
+ ret = av_write_frame(av_format_context, &av_packet);
if(ret < 0)
fprintf(stderr, "Error: Failed to write frame index %d to muxer, reason: %s (%d)\n", stream->index, av_error_to_string(ret), ret);
+
+ //av_packet_free(&av_packet);
}
if (av_write_trailer(av_format_context) != 0)
@@ -765,381 +1211,1131 @@ static void save_replay_async(AVCodecContext *video_codec_context, AVCodecContex
avio_close(av_format_context->pb);
avformat_free_context(av_format_context);
+ av_dict_free(&options);
+
+ for(AudioTrack &audio_track : audio_tracks) {
+ audio_track.stream = nullptr;
+ }
});
}
-static bool is_process_running_program(pid_t pid, const char *program_name) {
- char filepath[256];
- snprintf(filepath, sizeof(filepath), "/proc/%ld/exe", (long)pid);
+static void split_string(const std::string &str, char delimiter, std::function<bool(const char*,size_t)> callback) {
+ size_t index = 0;
+ while(index < str.size()) {
+ size_t end_index = str.find(delimiter, index);
+ if(end_index == std::string::npos)
+ end_index = str.size();
+
+ if(!callback(&str[index], end_index - index))
+ break;
+
+ index = end_index + 1;
+ }
+}
+
+static std::vector<AudioInput> parse_audio_input_arg(const char *str) {
+ std::vector<AudioInput> audio_inputs;
+ split_string(str, '|', [&audio_inputs](const char *sub, size_t size) {
+ AudioInput audio_input;
+ audio_input.name.assign(sub, size);
+ const size_t index = audio_input.name.find('/');
+ if(index != std::string::npos) {
+ audio_input.description = audio_input.name.substr(0, index);
+ audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + index + 1);
+ }
+ audio_inputs.push_back(std::move(audio_input));
+ return true;
+ });
+ return audio_inputs;
+}
- char resolved_path[PATH_MAX];
- const ssize_t resolved_path_len = readlink(filepath, resolved_path, sizeof(resolved_path) - 1);
- if(resolved_path_len == -1)
+// TODO: Does this match all livestreaming cases?
+static bool is_livestream_path(const char *str) {
+ const int len = strlen(str);
+ if((len >= 7 && memcmp(str, "http://", 7) == 0) || (len >= 8 && memcmp(str, "https://", 8) == 0))
+ return true;
+ else if((len >= 7 && memcmp(str, "rtmp://", 7) == 0) || (len >= 8 && memcmp(str, "rtmps://", 8) == 0))
+ return true;
+ else
return false;
+}
+
+// TODO: Proper cleanup
+static int init_filter_graph(AVCodecContext *audio_codec_context, AVFilterGraph **graph, AVFilterContext **sink, std::vector<AVFilterContext*> &src_filter_ctx, size_t num_sources) {
+ char ch_layout[64];
+ int err = 0;
+ ch_layout[0] = '\0';
+
+ AVFilterGraph *filter_graph = avfilter_graph_alloc();
+ if (!filter_graph) {
+ fprintf(stderr, "Unable to create filter graph.\n");
+ return AVERROR(ENOMEM);
+ }
+
+ for(size_t i = 0; i < num_sources; ++i) {
+ const AVFilter *abuffer = avfilter_get_by_name("abuffer");
+ if (!abuffer) {
+ fprintf(stderr, "Could not find the abuffer filter.\n");
+ return AVERROR_FILTER_NOT_FOUND;
+ }
- resolved_path[resolved_path_len] = '\0';
+ AVFilterContext *abuffer_ctx = avfilter_graph_alloc_filter(filter_graph, abuffer, NULL);
+ if (!abuffer_ctx) {
+ fprintf(stderr, "Could not allocate the abuffer instance.\n");
+ return AVERROR(ENOMEM);
+ }
+
+ #if LIBAVCODEC_VERSION_MAJOR < 60
+ av_get_channel_layout_string(ch_layout, sizeof(ch_layout), 0, AV_CH_LAYOUT_STEREO);
+ #else
+ av_channel_layout_describe(&audio_codec_context->ch_layout, ch_layout, sizeof(ch_layout));
+ #endif
+ av_opt_set (abuffer_ctx, "channel_layout", ch_layout, AV_OPT_SEARCH_CHILDREN);
+ av_opt_set (abuffer_ctx, "sample_fmt", av_get_sample_fmt_name(audio_codec_context->sample_fmt), AV_OPT_SEARCH_CHILDREN);
+ av_opt_set_q (abuffer_ctx, "time_base", audio_codec_context->time_base, AV_OPT_SEARCH_CHILDREN);
+ av_opt_set_int(abuffer_ctx, "sample_rate", audio_codec_context->sample_rate, AV_OPT_SEARCH_CHILDREN);
+ av_opt_set_int(abuffer_ctx, "bit_rate", audio_codec_context->bit_rate, AV_OPT_SEARCH_CHILDREN);
+
+ err = avfilter_init_str(abuffer_ctx, NULL);
+ if (err < 0) {
+ fprintf(stderr, "Could not initialize the abuffer filter.\n");
+ return err;
+ }
+
+ src_filter_ctx.push_back(abuffer_ctx);
+ }
+
+ const AVFilter *mix_filter = avfilter_get_by_name("amix");
+ if (!mix_filter) {
+ av_log(NULL, AV_LOG_ERROR, "Could not find the mix filter.\n");
+ return AVERROR_FILTER_NOT_FOUND;
+ }
+
+ char args[512];
+ snprintf(args, sizeof(args), "inputs=%d", (int)num_sources);
+
+ AVFilterContext *mix_ctx;
+ err = avfilter_graph_create_filter(&mix_ctx, mix_filter, "amix", args, NULL, filter_graph);
+ if (err < 0) {
+ av_log(NULL, AV_LOG_ERROR, "Cannot create audio amix filter\n");
+ return err;
+ }
+
+ const AVFilter *abuffersink = avfilter_get_by_name("abuffersink");
+ if (!abuffersink) {
+ fprintf(stderr, "Could not find the abuffersink filter.\n");
+ return AVERROR_FILTER_NOT_FOUND;
+ }
+
+ AVFilterContext *abuffersink_ctx = avfilter_graph_alloc_filter(filter_graph, abuffersink, "sink");
+ if (!abuffersink_ctx) {
+ fprintf(stderr, "Could not allocate the abuffersink instance.\n");
+ return AVERROR(ENOMEM);
+ }
+
+ err = avfilter_init_str(abuffersink_ctx, NULL);
+ if (err < 0) {
+ fprintf(stderr, "Could not initialize the abuffersink instance.\n");
+ return err;
+ }
+
+ err = 0;
+ for(size_t i = 0; i < src_filter_ctx.size(); ++i) {
+ AVFilterContext *src_ctx = src_filter_ctx[i];
+ if (err >= 0)
+ err = avfilter_link(src_ctx, 0, mix_ctx, i);
+ }
+ if (err >= 0)
+ err = avfilter_link(mix_ctx, 0, abuffersink_ctx, 0);
+ if (err < 0) {
+ av_log(NULL, AV_LOG_ERROR, "Error connecting filters\n");
+ return err;
+ }
+
+ err = avfilter_graph_config(filter_graph, NULL);
+ if (err < 0) {
+ av_log(NULL, AV_LOG_ERROR, "Error configuring the filter graph\n");
+ return err;
+ }
+
+ *graph = filter_graph;
+ *sink = abuffersink_ctx;
+
+ return 0;
+}
- const int program_name_len = strlen(program_name);
- return resolved_path_len >= program_name_len && memcmp(resolved_path + resolved_path_len - program_name_len, program_name, program_name_len) == 0;
+static void xwayland_check_callback(const gsr_monitor *monitor, void *userdata) {
+ bool *xwayland_found = (bool*)userdata;
+ if(monitor->name_len >= 8 && strncmp(monitor->name, "XWAYLAND", 8) == 0)
+ *xwayland_found = true;
+ else if(memmem(monitor->name, monitor->name_len, "X11", 3))
+ *xwayland_found = true;
}
-static void handle_existing_pid_file() {
- char buffer[256];
- int fd = open(pid_file, O_RDONLY);
- if(fd == -1)
- return;
+static bool is_xwayland(Display *display) {
+ int opcode, event, error;
+ if(XQueryExtension(display, "XWAYLAND", &opcode, &event, &error))
+ return true;
+
+ bool xwayland_found = false;
+ for_each_active_monitor_output_x11(display, xwayland_check_callback, &xwayland_found);
+ return xwayland_found;
+}
- ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
- if(bytes_read < 0) {
- perror("failed to read gpu-screen-recorder pid file");
- exit(1);
+static void list_supported_video_codecs() {
+ bool wayland = false;
+ Display *dpy = XOpenDisplay(nullptr);
+ if (!dpy) {
+ wayland = true;
+ fprintf(stderr, "Warning: failed to connect to the X server. Assuming wayland is running without Xwayland\n");
}
- buffer[bytes_read] = '\0';
- close(fd);
- long pid = 0;
- if(sscanf(buffer, "%ld %120s", &pid, buffer) == 2) {
- if(is_process_running_program(pid, "gpu-screen-recorder")) {
- fprintf(stderr, "Error: gpu-screen-recorder is already running\n");
- exit(1);
+ XSetErrorHandler(x11_error_handler);
+ XSetIOErrorHandler(x11_io_error_handler);
+
+ if(!wayland)
+ wayland = is_xwayland(dpy);
+
+ gsr_egl egl;
+ if(!gsr_egl_load(&egl, dpy, wayland, false)) {
+ fprintf(stderr, "gsr error: failed to load opengl\n");
+ _exit(1);
+ }
+
+ char card_path[128];
+ card_path[0] = '\0';
+ if(wayland || egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA) {
+ // TODO: Allow specifying another card, and in other places
+ if(!gsr_get_valid_card_path(&egl, card_path)) {
+ fprintf(stderr, "Error: no /dev/dri/cardX device found. If you are running GPU Screen Recorder with prime-run then try running without it\n");
+ _exit(2);
}
- } else {
- fprintf(stderr, "Warning: gpu-screen-recorder pid file is in incorrect format, it's possible that its corrupt. Replacing file and continuing...\n");
}
- unlink(pid_file);
+
+ av_log_set_level(AV_LOG_FATAL);
+
+ // TODO: Output hdr
+ if(find_h264_encoder(egl.gpu_info.vendor, card_path))
+ puts("h264");
+ if(find_h265_encoder(egl.gpu_info.vendor, card_path))
+ puts("hevc");
+ if(find_av1_encoder(egl.gpu_info.vendor, card_path))
+ puts("av1");
+
+ fflush(stdout);
+
+ gsr_egl_unload(&egl);
+ if(dpy)
+ XCloseDisplay(dpy);
}
-static void handle_new_pid_file(const char *mode) {
- int fd = open(pid_file, O_WRONLY|O_CREAT|O_TRUNC, 0777);
- if(fd == -1) {
- perror("failed to create gpu-screen-recorder pid file");
- exit(1);
+static gsr_capture* create_capture_impl(const char *window_str, const char *screen_region, bool wayland, gsr_gpu_info gpu_inf, gsr_egl &egl, int fps, bool overclock, VideoCodec video_codec, gsr_color_range color_range, bool record_cursor) {
+ vec2i region_size = { 0, 0 };
+ Window src_window_id = None;
+ bool follow_focused = false;
+
+ gsr_capture *capture = nullptr;
+ if(strcmp(window_str, "focused") == 0) {
+ if(wayland) {
+ fprintf(stderr, "Error: GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland\n");
+ _exit(2);
+ }
+
+ if(!screen_region) {
+ fprintf(stderr, "Error: option -s is required when using -w focused\n");
+ usage();
+ }
+
+ if(sscanf(screen_region, "%dx%d", &region_size.x, &region_size.y) != 2) {
+ fprintf(stderr, "Error: invalid value for option -s '%s', expected a value in format WxH\n", screen_region);
+ usage();
+ }
+
+ if(region_size.x <= 0 || region_size.y <= 0) {
+ fprintf(stderr, "Error: invalud value for option -s '%s', expected width and height to be greater than 0\n", screen_region);
+ usage();
+ }
+
+ follow_focused = true;
+ } else if(contains_non_hex_number(window_str)) {
+ if(wayland || egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA) {
+ if(strcmp(window_str, "screen") == 0) {
+ FirstOutputCallback first_output;
+ first_output.output_name = NULL;
+ for_each_active_monitor_output(&egl, GSR_CONNECTION_DRM, get_first_output, &first_output);
+
+ if(first_output.output_name) {
+ window_str = first_output.output_name;
+ } else {
+ fprintf(stderr, "Error: no available output found\n");
+ }
+ }
+
+ gsr_monitor gmon;
+ if(!get_monitor_by_name(&egl, GSR_CONNECTION_DRM, window_str, &gmon)) {
+ fprintf(stderr, "gsr error: display \"%s\" not found, expected one of:\n", window_str);
+ fprintf(stderr, " \"screen\"\n");
+ for_each_active_monitor_output(&egl, GSR_CONNECTION_DRM, monitor_output_callback_print, NULL);
+ _exit(1);
+ }
+ } else {
+ if(strcmp(window_str, "screen") != 0 && strcmp(window_str, "screen-direct") != 0 && strcmp(window_str, "screen-direct-force") != 0) {
+ gsr_monitor gmon;
+ if(!get_monitor_by_name(&egl, GSR_CONNECTION_X11, window_str, &gmon)) {
+ const int screens_width = XWidthOfScreen(DefaultScreenOfDisplay(egl.x11.dpy));
+ const int screens_height = XWidthOfScreen(DefaultScreenOfDisplay(egl.x11.dpy));
+ fprintf(stderr, "gsr error: display \"%s\" not found, expected one of:\n", window_str);
+ fprintf(stderr, " \"screen\" (%dx%d+%d+%d)\n", screens_width, screens_height, 0, 0);
+ fprintf(stderr, " \"screen-direct\" (%dx%d+%d+%d)\n", screens_width, screens_height, 0, 0);
+ fprintf(stderr, " \"screen-direct-force\" (%dx%d+%d+%d)\n", screens_width, screens_height, 0, 0);
+ for_each_active_monitor_output(&egl, GSR_CONNECTION_X11, monitor_output_callback_print, NULL);
+ _exit(1);
+ }
+ }
+ }
+
+ if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) {
+ if(wayland) {
+ gsr_capture_kms_cuda_params kms_params;
+ kms_params.egl = &egl;
+ kms_params.display_to_capture = window_str;
+ kms_params.gpu_inf = gpu_inf;
+ kms_params.hdr = video_codec_is_hdr(video_codec);
+ kms_params.color_range = color_range;
+ kms_params.record_cursor = record_cursor;
+ capture = gsr_capture_kms_cuda_create(&kms_params);
+ if(!capture)
+ _exit(1);
+ } else {
+ const char *capture_target = window_str;
+ bool direct_capture = strcmp(window_str, "screen-direct") == 0;
+ if(direct_capture) {
+ capture_target = "screen";
+ // TODO: Temporary disable direct capture because push model causes stuttering when it's direct capturing. This might be a nvfbc bug. This does not happen when using a compositor.
+ direct_capture = false;
+ fprintf(stderr, "Warning: screen-direct has temporary been disabled as it causes stuttering. This is likely a NvFBC bug. Falling back to \"screen\".\n");
+ }
+
+ if(strcmp(window_str, "screen-direct-force") == 0) {
+ direct_capture = true;
+ capture_target = "screen";
+ }
+
+ gsr_capture_nvfbc_params nvfbc_params;
+ nvfbc_params.egl = &egl;
+ nvfbc_params.display_to_capture = capture_target;
+ nvfbc_params.fps = fps;
+ nvfbc_params.pos = { 0, 0 };
+ nvfbc_params.size = { 0, 0 };
+ nvfbc_params.direct_capture = direct_capture;
+ nvfbc_params.overclock = overclock;
+ nvfbc_params.hdr = video_codec_is_hdr(video_codec);
+ nvfbc_params.color_range = color_range;
+ nvfbc_params.record_cursor = record_cursor;
+ capture = gsr_capture_nvfbc_create(&nvfbc_params);
+ if(!capture)
+ _exit(1);
+ }
+ } else {
+ gsr_capture_kms_vaapi_params kms_params;
+ kms_params.egl = &egl;
+ kms_params.display_to_capture = window_str;
+ kms_params.gpu_inf = gpu_inf;
+ kms_params.hdr = video_codec_is_hdr(video_codec);
+ kms_params.color_range = color_range;
+ kms_params.record_cursor = record_cursor;
+ capture = gsr_capture_kms_vaapi_create(&kms_params);
+ if(!capture)
+ _exit(1);
+ }
+ } else {
+ if(wayland) {
+ fprintf(stderr, "Error: GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland\n");
+ _exit(2);
+ }
+
+ errno = 0;
+ src_window_id = strtol(window_str, nullptr, 0);
+ if(src_window_id == None || errno == EINVAL) {
+ fprintf(stderr, "Invalid window number %s\n", window_str);
+ usage();
+ }
}
- char buffer[256];
- const int buffer_size = snprintf(buffer, sizeof(buffer), "%ld %s", (long)getpid(), mode);
- if(write(fd, buffer, buffer_size) == -1) {
- perror("failed to write gpu-screen-recorder pid file");
- exit(1);
+ if(!capture) {
+ switch(egl.gpu_info.vendor) {
+ case GSR_GPU_VENDOR_AMD:
+ case GSR_GPU_VENDOR_INTEL: {
+ gsr_capture_xcomposite_vaapi_params xcomposite_params;
+ xcomposite_params.base.egl = &egl;
+ xcomposite_params.base.window = src_window_id;
+ xcomposite_params.base.follow_focused = follow_focused;
+ xcomposite_params.base.region_size = region_size;
+ xcomposite_params.base.color_range = color_range;
+ xcomposite_params.base.record_cursor = record_cursor;
+ capture = gsr_capture_xcomposite_vaapi_create(&xcomposite_params);
+ if(!capture)
+ _exit(1);
+ break;
+ }
+ case GSR_GPU_VENDOR_NVIDIA: {
+ gsr_capture_xcomposite_cuda_params xcomposite_params;
+ xcomposite_params.base.egl = &egl;
+ xcomposite_params.base.window = src_window_id;
+ xcomposite_params.base.follow_focused = follow_focused;
+ xcomposite_params.base.region_size = region_size;
+ xcomposite_params.base.color_range = color_range;
+ xcomposite_params.base.record_cursor = record_cursor;
+ xcomposite_params.overclock = overclock;
+ capture = gsr_capture_xcomposite_cuda_create(&xcomposite_params);
+ if(!capture)
+ _exit(1);
+ break;
+ }
+ }
}
- close(fd);
+
+ return capture;
}
+struct Arg {
+ std::vector<const char*> values;
+ bool optional = false;
+ bool list = false;
+
+ const char* value() const {
+ if(values.empty())
+ return nullptr;
+ return values.front();
+ }
+};
+
int main(int argc, char **argv) {
- signal(SIGTERM, term_handler);
- signal(SIGINT, int_handler);
+ signal(SIGINT, stop_handler);
signal(SIGUSR1, save_replay_handler);
+ signal(SIGUSR2, toggle_pause_handler);
+
+ // Stop nvidia driver from buffering frames
+ setenv("__GL_MaxFramesAllowed", "1", true);
+ // If this is set to 1 then cuGraphicsGLRegisterImage will fail for egl context with error: invalid OpenGL or DirectX context,
+ // so we overwrite it
+ setenv("__GL_THREADED_OPTIMIZATIONS", "0", true);
+ // Some people set this to nvidia (for nvdec) or vdpau (for nvidia vdpau), which breaks gpu screen recorder since
+ // nvidia doesn't support vaapi and nvidia-vaapi-driver doesn't support encoding yet.
+ // Let vaapi find the match vaapi driver instead of forcing a specific one.
+ unsetenv("LIBVA_DRIVER_NAME");
+
+ if(argc <= 1)
+ usage_full();
+
+ if(argc == 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0))
+ usage_full();
+
+ if(argc == 2 && strcmp(argv[1], "--list-supported-video-codecs") == 0) {
+ list_supported_video_codecs();
+ _exit(0);
+ }
- handle_existing_pid_file();
+ //av_log_set_level(AV_LOG_TRACE);
std::map<std::string, Arg> args = {
- { "-w", Arg { nullptr, false } },
- //{ "-s", Arg { nullptr, true } },
- { "-c", Arg { nullptr, false } },
- { "-f", Arg { nullptr, false } },
- { "-s", Arg { nullptr, true } },
- { "-a", Arg { nullptr, true } },
- { "-q", Arg { nullptr, true } },
- { "-o", Arg { nullptr, true } },
- { "-r", Arg { nullptr, true } }
+ { "-w", Arg { {}, false, false } },
+ { "-c", Arg { {}, true, false } },
+ { "-f", Arg { {}, false, false } },
+ { "-s", Arg { {}, true, false } },
+ { "-a", Arg { {}, true, true } },
+ { "-q", Arg { {}, true, false } },
+ { "-o", Arg { {}, true, false } },
+ { "-r", Arg { {}, true, false } },
+ { "-k", Arg { {}, true, false } },
+ { "-ac", Arg { {}, true, false } },
+ { "-ab", Arg { {}, true, false } },
+ { "-oc", Arg { {}, true, false } },
+ { "-fm", Arg { {}, true, false } },
+ { "-pixfmt", Arg { {}, true, false } },
+ { "-v", Arg { {}, true, false } },
+ { "-mf", Arg { {}, true, false } },
+ { "-sc", Arg { {}, true, false } },
+ { "-cr", Arg { {}, true, false } },
+ { "-cursor", Arg { {}, true, false } },
};
- for(int i = 1; i < argc - 1; i += 2) {
+ for(int i = 1; i < argc; i += 2) {
auto it = args.find(argv[i]);
if(it == args.end()) {
fprintf(stderr, "Invalid argument '%s'\n", argv[i]);
usage();
}
- it->second.value = argv[i + 1];
+
+ if(!it->second.values.empty() && !it->second.list) {
+ fprintf(stderr, "Expected argument '%s' to only be specified once\n", argv[i]);
+ usage();
+ }
+
+ if(i + 1 >= argc) {
+ fprintf(stderr, "Missing value for argument '%s'\n", argv[i]);
+ usage();
+ }
+
+ it->second.values.push_back(argv[i + 1]);
}
for(auto &it : args) {
- if(!it.second.optional && !it.second.value) {
+ if(!it.second.optional && !it.second.value()) {
fprintf(stderr, "Missing argument '%s'\n", it.first.c_str());
usage();
}
}
- Arg &audio_input_arg = args["-a"];
+ VideoCodec video_codec = VideoCodec::HEVC;
+ const char *video_codec_to_use = args["-k"].value();
+ if(!video_codec_to_use)
+ video_codec_to_use = "auto";
+
+ if(strcmp(video_codec_to_use, "h264") == 0) {
+ video_codec = VideoCodec::H264;
+ } else if(strcmp(video_codec_to_use, "h265") == 0 || strcmp(video_codec_to_use, "hevc") == 0) {
+ video_codec = VideoCodec::HEVC;
+ } else if(strcmp(video_codec_to_use, "hevc_hdr") == 0) {
+ video_codec = VideoCodec::HEVC_HDR;
+ } else if(strcmp(video_codec_to_use, "av1") == 0) {
+ video_codec = VideoCodec::AV1;
+ } else if(strcmp(video_codec_to_use, "av1_hdr") == 0) {
+ video_codec = VideoCodec::AV1_HDR;
+ } else if(strcmp(video_codec_to_use, "auto") != 0) {
+ fprintf(stderr, "Error: -k should either be either 'auto', 'h264', 'hevc', 'hevc_hdr', 'av1' or 'av1_hdr', got: '%s'\n", video_codec_to_use);
+ usage();
+ }
+
+ AudioCodec audio_codec = AudioCodec::AAC;
+ const char *audio_codec_to_use = args["-ac"].value();
+ if(!audio_codec_to_use)
+ audio_codec_to_use = "aac";
+
+ if(strcmp(audio_codec_to_use, "aac") == 0) {
+ audio_codec = AudioCodec::AAC;
+ } else if(strcmp(audio_codec_to_use, "opus") == 0) {
+ audio_codec = AudioCodec::OPUS;
+ } else if(strcmp(audio_codec_to_use, "flac") == 0) {
+ audio_codec = AudioCodec::FLAC;
+ } else {
+ fprintf(stderr, "Error: -ac should either be either 'aac', 'opus' or 'flac', got: '%s'\n", audio_codec_to_use);
+ usage();
+ }
+
+ if(audio_codec == AudioCodec::OPUS || audio_codec == AudioCodec::FLAC) {
+ fprintf(stderr, "Warning: opus and flac audio codecs are temporary disabled, using aac audio codec instead\n");
+ audio_codec_to_use = "aac";
+ audio_codec = AudioCodec::AAC;
+ }
+
+ int audio_bitrate = 0;
+ const char *audio_bitrate_str = args["-ab"].value();
+ if(audio_bitrate_str) {
+ if(sscanf(audio_bitrate_str, "%d", &audio_bitrate) != 1) {
+ fprintf(stderr, "Error: -ab argument \"%s\" is not an integer\n", audio_bitrate_str);
+ usage();
+ }
+ }
+
+ bool overclock = false;
+ const char *overclock_str = args["-oc"].value();
+ if(!overclock_str)
+ overclock_str = "no";
+
+ if(strcmp(overclock_str, "yes") == 0) {
+ overclock = true;
+ } else if(strcmp(overclock_str, "no") == 0) {
+ overclock = false;
+ } else {
+ fprintf(stderr, "Error: -oc should either be either 'yes' or 'no', got: '%s'\n", overclock_str);
+ usage();
+ }
+
+ bool verbose = true;
+ const char *verbose_str = args["-v"].value();
+ if(!verbose_str)
+ verbose_str = "yes";
+
+ if(strcmp(verbose_str, "yes") == 0) {
+ verbose = true;
+ } else if(strcmp(verbose_str, "no") == 0) {
+ verbose = false;
+ } else {
+ fprintf(stderr, "Error: -v should either be either 'yes' or 'no', got: '%s'\n", verbose_str);
+ usage();
+ }
+
+ bool record_cursor = true;
+ const char *record_cursor_str = args["-cursor"].value();
+ if(!record_cursor_str)
+ record_cursor_str = "yes";
+
+ if(strcmp(record_cursor_str, "yes") == 0) {
+ record_cursor = true;
+ } else if(strcmp(record_cursor_str, "no") == 0) {
+ record_cursor = false;
+ } else {
+ fprintf(stderr, "Error: -cursor should either be either 'yes' or 'no', got: '%s'\n", record_cursor_str);
+ usage();
+ }
+
+ bool make_folders = false;
+ const char *make_folders_str = args["-mf"].value();
+ if(!make_folders_str)
+ make_folders_str = "no";
+
+ if(strcmp(make_folders_str, "yes") == 0) {
+ make_folders = true;
+ } else if(strcmp(make_folders_str, "no") == 0) {
+ make_folders = false;
+ } else {
+ fprintf(stderr, "Error: -mf should either be either 'yes' or 'no', got: '%s'\n", make_folders_str);
+ usage();
+ }
+
+ const char *recording_saved_script = args["-sc"].value();
+ if(recording_saved_script) {
+ struct stat buf;
+ if(stat(recording_saved_script, &buf) == -1 || !S_ISREG(buf.st_mode)) {
+ fprintf(stderr, "Error: Script \"%s\" either doesn't exist or it's not a file\n", recording_saved_script);
+ usage();
+ }
+
+ if(!(buf.st_mode & S_IXUSR)) {
+ fprintf(stderr, "Error: Script \"%s\" is not executable\n", recording_saved_script);
+ usage();
+ }
+ }
+
+ PixelFormat pixel_format = PixelFormat::YUV420;
+ const char *pixfmt = args["-pixfmt"].value();
+ if(!pixfmt)
+ pixfmt = "yuv420";
- uint32_t region_x = 0;
- uint32_t region_y = 0;
- uint32_t region_width = 0;
- uint32_t region_height = 0;
+ if(strcmp(pixfmt, "yuv420") == 0) {
+ pixel_format = PixelFormat::YUV420;
+ } else if(strcmp(pixfmt, "yuv444") == 0) {
+ pixel_format = PixelFormat::YUV444;
+ } else {
+ fprintf(stderr, "Error: -pixfmt should either be either 'yuv420', or 'yuv444', got: '%s'\n", pixfmt);
+ usage();
+ }
+
+ const Arg &audio_input_arg = args["-a"];
+ std::vector<AudioInput> audio_inputs;
+ if(!audio_input_arg.values.empty())
+ audio_inputs = get_pulseaudio_inputs();
+ std::vector<MergedAudioInputs> requested_audio_inputs;
+ bool uses_amix = false;
+
+ // Manually check if the audio inputs we give exist. This is only needed for pipewire, not pulseaudio.
+ // Pipewire instead DEFAULTS TO THE DEFAULT AUDIO INPUT. THAT'S RETARDED.
+ // OH, YOU MISSPELLED THE AUDIO INPUT? FUCK YOU
+ for(const char *audio_input : audio_input_arg.values) {
+ if(!audio_input || audio_input[0] == '\0')
+ continue;
- /*
- TODO: Fix this. Doesn't work for some reason
- const char *screen_region = args["-s"].value;
- if(screen_region) {
- if(sscanf(screen_region, "%ux%u+%u+%u", &region_x, &region_y, &region_width, &region_height) != 4) {
- fprintf(stderr, "Invalid value for -s '%s', expected a value in format WxH+X+Y\n", screen_region);
- return 1;
+ requested_audio_inputs.push_back({parse_audio_input_arg(audio_input)});
+ if(requested_audio_inputs.back().audio_inputs.size() > 1)
+ uses_amix = true;
+
+ for(AudioInput &request_audio_input : requested_audio_inputs.back().audio_inputs) {
+ bool match = false;
+ for(const auto &existing_audio_input : audio_inputs) {
+ if(strcmp(request_audio_input.name.c_str(), existing_audio_input.name.c_str()) == 0) {
+ if(request_audio_input.description.empty())
+ request_audio_input.description = "gsr-" + existing_audio_input.description;
+
+ match = true;
+ break;
+ }
+ }
+
+ if(!match) {
+ fprintf(stderr, "Error: Audio input device '%s' is not a valid audio device, expected one of:\n", request_audio_input.name.c_str());
+ for(const auto &existing_audio_input : audio_inputs) {
+ fprintf(stderr, " %s (%s)\n", existing_audio_input.name.c_str(), existing_audio_input.description.c_str());
+ }
+ _exit(2);
+ }
}
}
- */
- const char *container_format = args["-c"].value;
- int fps = atoi(args["-f"].value);
+ const char *container_format = args["-c"].value();
+ if(container_format && strcmp(container_format, "mkv") == 0)
+ container_format = "matroska";
+
+ int fps = atoi(args["-f"].value());
if(fps == 0) {
- fprintf(stderr, "Invalid fps argument: %s\n", args["-f"].value);
- return 1;
+ fprintf(stderr, "Invalid fps argument: %s\n", args["-f"].value());
+ _exit(1);
}
- if(fps > 250)
- fps = 250;
+ if(fps < 1)
+ fps = 1;
- const char *quality_str = args["-q"].value;
+ const char *quality_str = args["-q"].value();
if(!quality_str)
- quality_str = "medium";
+ quality_str = "very_high";
VideoQuality quality;
if(strcmp(quality_str, "medium") == 0) {
quality = VideoQuality::MEDIUM;
} else if(strcmp(quality_str, "high") == 0) {
quality = VideoQuality::HIGH;
+ } else if(strcmp(quality_str, "very_high") == 0) {
+ quality = VideoQuality::VERY_HIGH;
} else if(strcmp(quality_str, "ultra") == 0) {
quality = VideoQuality::ULTRA;
} else {
- fprintf(stderr, "Error: -q should either be either 'medium', 'high' or 'ultra', got: '%s'\n", quality_str);
+ fprintf(stderr, "Error: -q should either be either 'medium', 'high', 'very_high' or 'ultra', got: '%s'\n", quality_str);
usage();
}
int replay_buffer_size_secs = -1;
- const char *replay_buffer_size_secs_str = args["-r"].value;
+ const char *replay_buffer_size_secs_str = args["-r"].value();
if(replay_buffer_size_secs_str) {
replay_buffer_size_secs = atoi(replay_buffer_size_secs_str);
if(replay_buffer_size_secs < 5 || replay_buffer_size_secs > 1200) {
fprintf(stderr, "Error: option -r has to be between 5 and 1200, was: %s\n", replay_buffer_size_secs_str);
- return 1;
+ _exit(1);
}
- replay_buffer_size_secs += 5; // Add a few seconds to account of lost packets because of non-keyframe packets skipped
+ replay_buffer_size_secs += 3; // Add a few seconds to account of lost packets because of non-keyframe packets skipped
}
- CUresult res;
+ const char *window_str = strdup(args["-w"].value());
- res = cuInit(0);
- if(res != CUDA_SUCCESS) {
- const char *err_str;
- cuGetErrorString(res, &err_str);
- fprintf(stderr, "Error: cuInit failed, error %s (result: %d)\n", err_str, res);
- return 1;
+ bool wayland = false;
+ Display *dpy = XOpenDisplay(nullptr);
+ if (!dpy) {
+ wayland = true;
+ fprintf(stderr, "Warning: failed to connect to the X server. Assuming wayland is running without Xwayland\n");
}
- int nGpu = 0;
- cuDeviceGetCount(&nGpu);
- if (nGpu <= 0) {
- fprintf(stderr, "Error: no cuda supported devices found\n");
- return 1;
+ XSetErrorHandler(x11_error_handler);
+ XSetIOErrorHandler(x11_io_error_handler);
+
+ if(!wayland)
+ wayland = is_xwayland(dpy);
+
+ if(video_codec_is_hdr(video_codec) && !wayland) {
+ fprintf(stderr, "Error: hdr video codec option %s is not available on X11\n", video_codec_to_use);
+ _exit(1);
}
- CUdevice cu_dev;
- res = cuDeviceGet(&cu_dev, 0);
- if(res != CUDA_SUCCESS) {
- const char *err_str;
- cuGetErrorString(res, &err_str);
- fprintf(stderr, "Error: unable to get CUDA device, error: %s (result: %d)\n", err_str, res);
- return 1;
+ const bool is_monitor_capture = strcmp(window_str, "focused") != 0 && contains_non_hex_number(window_str);
+ gsr_egl egl;
+ if(!gsr_egl_load(&egl, dpy, wayland, is_monitor_capture)) {
+ fprintf(stderr, "gsr error: failed to load opengl\n");
+ _exit(1);
}
- CUcontext cu_ctx;
- res = cuCtxCreate_v2(&cu_ctx, CU_CTX_SCHED_AUTO, cu_dev);
- if(res != CUDA_SUCCESS) {
- const char *err_str;
- cuGetErrorString(res, &err_str);
- fprintf(stderr, "Error: unable to create CUDA context, error: %s (result: %d)\n", err_str, res);
- return 1;
+ bool very_old_gpu = false;
+
+ if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA && egl.gpu_info.gpu_version != 0 && egl.gpu_info.gpu_version < 900) {
+ fprintf(stderr, "Info: your gpu appears to be very old (older than maxwell architecture). Switching to lower preset\n");
+ very_old_gpu = true;
}
- uint32_t window_width = 0;
- uint32_t window_height = 0;
+ if(egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA && overclock) {
+ fprintf(stderr, "Info: overclock option has no effect on amd/intel, ignoring option\n");
+ }
- NvFBCLibrary nv_fbc_library;
+ if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA && overclock && wayland) {
+ fprintf(stderr, "Info: overclocking is not possible on nvidia on wayland, ignoring option\n");
+ }
- const char *window_str = args["-w"].value;
- Window src_window_id = None;
- if(contains_non_hex_number(window_str)) {
- if(!nv_fbc_library.load())
- return 1;
+ egl.card_path[0] = '\0';
+ if(wayland || egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA) {
+ // TODO: Allow specifying another card, and in other places
+ if(!gsr_get_valid_card_path(&egl, egl.card_path)) {
+ fprintf(stderr, "Error: no /dev/dri/cardX device found. If you are running GPU Screen Recorder with prime-run then try running without it\n");
+ _exit(2);
+ }
+ }
+
+ // TODO: Fix constant framerate not working properly on amd/intel because capture framerate gets locked to the same framerate as
+ // game framerate, which doesn't work well when you need to encode multiple duplicate frames (AMD/Intel is slow at encoding!).
+ // It also appears to skip audio frames on nvidia wayland? why? that should be fine, but it causes video stuttering because of audio/video sync.
+ FramerateMode framerate_mode;
+ const char *framerate_mode_str = args["-fm"].value();
+ if(!framerate_mode_str)
+ framerate_mode_str = "vfr";
+
+ if(strcmp(framerate_mode_str, "cfr") == 0) {
+ framerate_mode = FramerateMode::CONSTANT;
+ } else if(strcmp(framerate_mode_str, "vfr") == 0) {
+ framerate_mode = FramerateMode::VARIABLE;
+ } else {
+ fprintf(stderr, "Error: -fm should either be either 'cfr' or 'vfr', got: '%s'\n", framerate_mode_str);
+ usage();
+ }
- const char *capture_target = window_str;
- const bool direct_capture = strcmp(window_str, "screen-direct") == 0;
- if(direct_capture)
- capture_target = "screen";
+ gsr_color_range color_range;
+ const char *color_range_str = args["-cr"].value();
+ if(!color_range_str)
+ color_range_str = "limited";
- if(!nv_fbc_library.create(capture_target, fps, &window_width, &window_height, region_x, region_y, region_width, region_height, direct_capture))
- return 1;
+ if(strcmp(color_range_str, "limited") == 0) {
+ color_range = GSR_COLOR_RANGE_LIMITED;
+ } else if(strcmp(color_range_str, "full") == 0) {
+ color_range = GSR_COLOR_RANGE_FULL;
} else {
- errno = 0;
- src_window_id = strtol(window_str, nullptr, 0);
- if(src_window_id == None || errno == EINVAL) {
- fprintf(stderr, "Invalid window number %s\n", window_str);
- usage();
- }
+ fprintf(stderr, "Error: -cr should either be either 'limited' or 'full', got: '%s'\n", color_range_str);
+ usage();
}
- int record_width = window_width;
- int record_height = window_height;
- const char *record_area = args["-s"].value;
- if(record_area) {
- if(sscanf(record_area, "%dx%d", &record_width, &record_height) != 2) {
- fprintf(stderr, "Invalid value for -s '%s', expected a value in format WxH\n", record_area);
- return 1;
- }
+ const char *screen_region = args["-s"].value();
+
+ if(screen_region && strcmp(window_str, "focused") != 0) {
+ fprintf(stderr, "Error: option -s is only available when using -w focused\n");
+ usage();
}
- const char *filename = args["-o"].value;
+ bool is_livestream = false;
+ const char *filename = args["-o"].value();
if(filename) {
- if(replay_buffer_size_secs != -1) {
- struct stat buf;
- if(stat(filename, &buf) == -1 || !S_ISDIR(buf.st_mode)) {
- fprintf(stderr, "%s does not exist or is not a directory\n", filename);
- usage();
+ is_livestream = is_livestream_path(filename);
+ if(is_livestream) {
+ if(replay_buffer_size_secs != -1) {
+ fprintf(stderr, "Error: replay mode is not applicable to live streaming\n");
+ _exit(1);
+ }
+ } else {
+ if(replay_buffer_size_secs == -1) {
+ char directory_buf[PATH_MAX];
+ strcpy(directory_buf, filename);
+ char *directory = dirname(directory_buf);
+ if(strcmp(directory, ".") != 0 && strcmp(directory, "/") != 0) {
+ if(create_directory_recursive(directory) != 0) {
+ fprintf(stderr, "Error: failed to create directory for output file: %s\n", filename);
+ _exit(1);
+ }
+ }
+ } else {
+ if(!container_format) {
+ fprintf(stderr, "Error: option -c is required when using option -r\n");
+ usage();
+ }
+
+ struct stat buf;
+ if(stat(filename, &buf) != -1 && !S_ISDIR(buf.st_mode)) {
+ fprintf(stderr, "Error: File \"%s\" exists but it's not a directory\n", filename);
+ usage();
+ }
}
}
} else {
if(replay_buffer_size_secs == -1) {
filename = "/dev/stdout";
} else {
- fprintf(stderr, "Option -o is required when using option -r\n");
+ fprintf(stderr, "Error: Option -o is required when using option -r\n");
usage();
}
- }
- const double target_fps = 1.0 / (double)fps;
-
- WindowPixmap window_pixmap;
- Display *dpy = nullptr;
- GLFWwindow *window = nullptr;
- if(src_window_id) {
- dpy = XOpenDisplay(nullptr);
- if (!dpy) {
- fprintf(stderr, "Error: Failed to open display\n");
- return 1;
+ if(!container_format) {
+ fprintf(stderr, "Error: option -c is required when not using option -o\n");
+ usage();
}
+ }
- bool has_name_pixmap = x11_supports_composite_named_window_pixmap(dpy);
- if (!has_name_pixmap) {
- fprintf(stderr, "Error: XCompositeNameWindowPixmap is not supported by "
- "your X11 server\n");
- return 1;
- }
+ AVFormatContext *av_format_context;
+ // The output format is automatically guessed by the file extension
+ avformat_alloc_output_context2(&av_format_context, nullptr, container_format, filename);
+ if (!av_format_context) {
+ if(container_format)
+ fprintf(stderr, "Error: Container format '%s' (argument -c) is not valid\n", container_format);
+ else
+ fprintf(stderr, "Error: Failed to deduce container format from file extension\n");
+ _exit(1);
+ }
- XWindowAttributes attr;
- if (!XGetWindowAttributes(dpy, src_window_id, &attr)) {
- fprintf(stderr, "Error: Invalid window id: %lu\n", src_window_id);
- return 1;
- }
+ const AVOutputFormat *output_format = av_format_context->oformat;
- window_width = attr.width;
- window_height = attr.height;
+ std::string file_extension = output_format->extensions;
+ {
+ size_t comma_index = file_extension.find(',');
+ if(comma_index != std::string::npos)
+ file_extension = file_extension.substr(0, comma_index);
+ }
- XCompositeRedirectWindow(dpy, src_window_id, CompositeRedirectAutomatic);
+ if(egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA && file_extension == "mkv" && strcmp(video_codec_to_use, "h264") == 0) {
+ video_codec_to_use = "hevc";
+ video_codec = VideoCodec::HEVC;
+ fprintf(stderr, "Warning: video codec was forcefully set to hevc because mkv container is used and mesa (AMD and Intel driver) does not support h264 in mkv files\n");
+ }
- // glXMakeContextCurrent(Display *dpy, GLXDrawable draw, GLXDrawable read,
- // GLXContext ctx)
- if (!glfwInit()) {
- fprintf(stderr, "Error: Failed to initialize glfw\n");
- return 1;
+ switch(audio_codec) {
+ case AudioCodec::AAC: {
+ break;
}
-
- glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
- glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
- glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
- glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
- glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
- glfwWindowHint(GLFW_VISIBLE, GL_FALSE);
-
- window = glfwCreateWindow(1, 1, "gpu-screen-recorder", nullptr, nullptr);
- if (!window) {
- fprintf(stderr, "Error: Failed to create glfw window\n");
- glfwTerminate();
- return 1;
+ case AudioCodec::OPUS: {
+ // TODO: Also check mpegts?
+ if(file_extension != "mp4" && file_extension != "mkv") {
+ audio_codec_to_use = "aac";
+ audio_codec = AudioCodec::AAC;
+ fprintf(stderr, "Warning: opus audio codec is only supported by .mp4 and .mkv files, falling back to aac instead\n");
+ }
+ break;
+ }
+ case AudioCodec::FLAC: {
+ // TODO: Also check mpegts?
+ if(file_extension != "mp4" && file_extension != "mkv") {
+ audio_codec_to_use = "aac";
+ audio_codec = AudioCodec::AAC;
+ fprintf(stderr, "Warning: flac audio codec is only supported by .mp4 and .mkv files, falling back to aac instead\n");
+ } else if(uses_amix) {
+ audio_codec_to_use = "opus";
+ audio_codec = AudioCodec::OPUS;
+ fprintf(stderr, "Warning: flac audio codec is not supported when mixing audio sources, falling back to opus instead\n");
+ }
+ break;
}
+ }
- glfwMakeContextCurrent(window);
- glfwSwapInterval(0);
+ const double target_fps = 1.0 / (double)fps;
- //#if defined(DEBUG)
- XSetErrorHandler(x11_error_handler);
- XSetIOErrorHandler(x11_io_error_handler);
- //#endif
+ const bool video_codec_auto = strcmp(video_codec_to_use, "auto") == 0;
+ if(video_codec_auto) {
+ if(egl.gpu_info.vendor == GSR_GPU_VENDOR_INTEL) {
+ const AVCodec *h264_codec = find_h264_encoder(egl.gpu_info.vendor, egl.card_path);
+ if(!h264_codec) {
+ fprintf(stderr, "Info: using hevc encoder because a codec was not specified and your gpu does not support h264\n");
+ video_codec_to_use = "hevc";
+ video_codec = VideoCodec::HEVC;
+ } else {
+ fprintf(stderr, "Info: using h264 encoder because a codec was not specified\n");
+ video_codec_to_use = "h264";
+ video_codec = VideoCodec::H264;
+ }
+ } else {
+ const AVCodec *h265_codec = find_h265_encoder(egl.gpu_info.vendor, egl.card_path);
- glewExperimental = GL_TRUE;
- GLenum nGlewError = glewInit();
- if (nGlewError != GLEW_OK) {
- fprintf(stderr, "%s - Error initializing GLEW! %s\n", __FUNCTION__,
- glewGetErrorString(nGlewError));
- return 1;
- }
- glGetError(); // to clear the error caused deep in GLEW
+ if(h265_codec && fps > 60) {
+ fprintf(stderr, "Warning: recording at higher fps than 60 with hevc might result in recording at a very low fps. If this happens, switch to h264 or av1\n");
+ }
- if (!recreate_window_pixmap(dpy, src_window_id, window_pixmap)) {
- fprintf(stderr, "Error: Failed to create glx pixmap for window: %lu\n",
- src_window_id);
- return 1;
+ // hevc generally allows recording at a higher resolution than h264 on nvidia cards. On a gtx 1080 4k is the max resolution for h264 but for hevc it's 8k.
+ // Another important info is that when recording at a higher fps than.. 60? hevc has very bad performance. For example when recording at 144 fps the fps drops to 1
+ // while with h264 the fps doesn't drop.
+ if(!h265_codec) {
+ fprintf(stderr, "Info: using h264 encoder because a codec was not specified and your gpu does not support hevc\n");
+ video_codec_to_use = "h264";
+ video_codec = VideoCodec::H264;
+ } else {
+ fprintf(stderr, "Info: using hevc encoder because a codec was not specified\n");
+ video_codec_to_use = "hevc";
+ video_codec = VideoCodec::HEVC;
+ }
}
+ }
- if(!record_area) {
- record_width = window_pixmap.texture_width;
- record_height = window_pixmap.texture_height;
- fprintf(stderr, "Record size: %dx%d\n", record_width, record_height);
+ // TODO: Allow hevc, vp9 and av1 in (enhanced) flv (supported since ffmpeg 6.1)
+ const bool is_flv = strcmp(file_extension.c_str(), "flv") == 0;
+ if(video_codec != VideoCodec::H264 && is_flv) {
+ video_codec_to_use = "h264";
+ video_codec = VideoCodec::H264;
+ fprintf(stderr, "Warning: hevc/av1 is not compatible with flv, falling back to h264 instead.\n");
+ }
+
+ const AVCodec *video_codec_f = nullptr;
+ switch(video_codec) {
+ case VideoCodec::H264:
+ video_codec_f = find_h264_encoder(egl.gpu_info.vendor, egl.card_path);
+ break;
+ case VideoCodec::HEVC:
+ case VideoCodec::HEVC_HDR:
+ video_codec_f = find_h265_encoder(egl.gpu_info.vendor, egl.card_path);
+ break;
+ case VideoCodec::AV1:
+ case VideoCodec::AV1_HDR:
+ video_codec_f = find_av1_encoder(egl.gpu_info.vendor, egl.card_path);
+ break;
+ }
+
+ if(!video_codec_auto && !video_codec_f && !is_flv) {
+ switch(video_codec) {
+ case VideoCodec::H264: {
+ fprintf(stderr, "Warning: selected video codec h264 is not supported, trying hevc instead\n");
+ video_codec_to_use = "hevc";
+ video_codec = VideoCodec::HEVC;
+ video_codec_f = find_h265_encoder(egl.gpu_info.vendor, egl.card_path);
+ break;
+ }
+ case VideoCodec::HEVC:
+ case VideoCodec::HEVC_HDR: {
+ fprintf(stderr, "Warning: selected video codec hevc is not supported, trying h264 instead\n");
+ video_codec_to_use = "h264";
+ video_codec = VideoCodec::H264;
+ video_codec_f = find_h264_encoder(egl.gpu_info.vendor, egl.card_path);
+ break;
+ }
+ case VideoCodec::AV1:
+ case VideoCodec::AV1_HDR: {
+ fprintf(stderr, "Warning: selected video codec av1 is not supported, trying h264 instead\n");
+ video_codec_to_use = "h264";
+ video_codec = VideoCodec::H264;
+ video_codec_f = find_h264_encoder(egl.gpu_info.vendor, egl.card_path);
+ break;
+ }
}
- } else {
- window_pixmap.texture_id = 0;
- window_pixmap.target_texture_id = 0;
- window_pixmap.texture_width = window_width;
- window_pixmap.texture_height = window_height;
+ }
- if (!glfwInit()) {
- fprintf(stderr, "Error: Failed to initialize glfw\n");
- return 1;
+ if(!video_codec_f) {
+ const char *video_codec_name = "";
+ switch(video_codec) {
+ case VideoCodec::H264: {
+ video_codec_name = "h264";
+ break;
+ }
+ case VideoCodec::HEVC:
+ case VideoCodec::HEVC_HDR: {
+ video_codec_name = "hevc";
+ break;
+ }
+ case VideoCodec::AV1:
+ case VideoCodec::AV1_HDR: {
+ video_codec_name = "av1";
+ break;
+ }
}
+
+ fprintf(stderr, "Error: your gpu does not support '%s' video codec. If you are sure that your gpu does support '%s' video encoding and you are using an AMD/Intel GPU,\n"
+ " then make sure you have installed the GPU specific vaapi packages (intel-media-driver, libva-intel-driver or libva-mesa-driver).\n"
+ " It's also possible that your distro has disabled hardware accelerated video encoding for '%s' video codec.\n"
+ " This may be the case on corporate distros such as Manjaro, Fedora or OpenSUSE.\n"
+ " You can test this by running 'vainfo | grep VAEntrypointEncSlice' to see if it matches any H264/HEVC profile.\n"
+ " On such distros, you need to manually install mesa from source to enable H264/HEVC hardware acceleration, or use a more user friendly distro. Alternatively record with AV1 if supported by your GPU.\n"
+ " You can alternatively use the flatpak version of GPU Screen Recorder (https://flathub.org/apps/com.dec05eba.gpu_screen_recorder) which bypasses system issues with patented H264/HEVC codecs.\n"
+ " Make sure you have mesa-extra freedesktop runtime installed when using the flatpak (this should be the default), which can be installed with this command:\n"
+ " flatpak install --system org.freedesktop.Platform.GL.default//23.08-extra", video_codec_name, video_codec_name, video_codec_name);
+ _exit(2);
}
- // Video start
- AVFormatContext *av_format_context;
- // The output format is automatically guessed by the file extension
- avformat_alloc_output_context2(&av_format_context, nullptr, container_format,
- nullptr);
- if (!av_format_context) {
- fprintf(
- stderr,
- "Error: Failed to deduce output format from file extension\n");
- return 1;
+ gsr_capture *capture = create_capture_impl(window_str, screen_region, wayland, egl.gpu_info, egl, fps, overclock, video_codec, color_range, record_cursor);
+
+ // (Some?) livestreaming services require at least one audio track to work.
+ // If not audio is provided then create one silent audio track.
+ if(is_livestream && requested_audio_inputs.empty()) {
+ fprintf(stderr, "Info: live streaming but no audio track was added. Adding a silent audio track\n");
+ MergedAudioInputs mai;
+ mai.audio_inputs.push_back({ "", "gsr-silent" });
+ requested_audio_inputs.push_back(std::move(mai));
}
- av_format_context->flags |= AVFMT_FLAG_GENPTS;
- const AVOutputFormat *output_format = av_format_context->oformat;
+ if(is_livestream && framerate_mode != FramerateMode::CONSTANT) {
+ fprintf(stderr, "Info: framerate mode was forcefully set to \"cfr\" because live streaming was detected\n");
+ framerate_mode = FramerateMode::CONSTANT;
+ framerate_mode_str = "cfr";
+ }
- //bool use_hevc = strcmp(window_str, "screen") == 0 || strcmp(window_str, "screen-direct") == 0;
- bool use_hevc = true;
- if(use_hevc && strcmp(container_format, "flv") == 0) {
- use_hevc = false;
- fprintf(stderr, "Warning: hevc is not compatible with flv, falling back to h264 instead.\n");
+ if(is_livestream && recording_saved_script) {
+ fprintf(stderr, "Warning: live stream detected, -sc script is ignored\n");
+ recording_saved_script = nullptr;
}
AVStream *video_stream = nullptr;
- AVStream *audio_stream = nullptr;
+ std::vector<AudioTrack> audio_tracks;
+ const bool hdr = video_codec_is_hdr(video_codec);
- AVCodecContext *video_codec_context = create_video_codec_context(av_format_context, quality, record_width, record_height, fps, use_hevc);
+ AVCodecContext *video_codec_context = create_video_codec_context(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA ? AV_PIX_FMT_CUDA : AV_PIX_FMT_VAAPI, quality, fps, video_codec_f, is_livestream, egl.gpu_info.vendor, framerate_mode, hdr, color_range);
if(replay_buffer_size_secs == -1)
video_stream = create_stream(av_format_context, video_codec_context);
- AVBufferRef *device_ctx;
- CUgraphicsResource cuda_graphics_resource;
- open_video(video_codec_context, window_pixmap, &device_ctx, &cuda_graphics_resource, cu_ctx);
+ AVFrame *video_frame = av_frame_alloc();
+ if(!video_frame) {
+ fprintf(stderr, "Error: Failed to allocate video frame\n");
+ _exit(1);
+ }
+ video_frame->format = video_codec_context->pix_fmt;
+ video_frame->width = video_codec_context->width;
+ video_frame->height = video_codec_context->height;
+ video_frame->color_range = video_codec_context->color_range;
+ video_frame->color_primaries = video_codec_context->color_primaries;
+ video_frame->color_trc = video_codec_context->color_trc;
+ video_frame->colorspace = video_codec_context->colorspace;
+ video_frame->chroma_location = video_codec_context->chroma_sample_location;
+
+ int capture_result = gsr_capture_start(capture, video_codec_context, video_frame);
+ if(capture_result != 0) {
+ fprintf(stderr, "gsr error: gsr_capture_start failed\n");
+ _exit(capture_result);
+ }
+
+ open_video(video_codec_context, quality, very_old_gpu, egl.gpu_info.vendor, pixel_format, hdr);
if(video_stream)
avcodec_parameters_from_context(video_stream->codecpar, video_codec_context);
- AVCodecContext *audio_codec_context = nullptr;
- AVFrame *audio_frame = nullptr;
- if(audio_input_arg.value) {
- audio_codec_context = create_audio_codec_context(av_format_context, fps);
+ int audio_stream_index = VIDEO_STREAM_INDEX + 1;
+ for(const MergedAudioInputs &merged_audio_inputs : requested_audio_inputs) {
+ const bool use_amix = merged_audio_inputs.audio_inputs.size() > 1;
+ AVCodecContext *audio_codec_context = create_audio_codec_context(fps, audio_codec, use_amix, audio_bitrate);
+
+ AVStream *audio_stream = nullptr;
if(replay_buffer_size_secs == -1)
audio_stream = create_stream(av_format_context, audio_codec_context);
- audio_frame = open_audio(audio_codec_context);
+ open_audio(audio_codec_context);
if(audio_stream)
avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_context);
+
+ #if LIBAVCODEC_VERSION_MAJOR < 60
+ const int num_channels = audio_codec_context->channels;
+ #else
+ const int num_channels = audio_codec_context->ch_layout.nb_channels;
+ #endif
+
+ //audio_frame->sample_rate = audio_codec_context->sample_rate;
+
+ std::vector<AVFilterContext*> src_filter_ctx;
+ AVFilterGraph *graph = nullptr;
+ AVFilterContext *sink = nullptr;
+ if(use_amix) {
+ int err = init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs.audio_inputs.size());
+ if(err < 0) {
+ fprintf(stderr, "Error: failed to create audio filter\n");
+ _exit(1);
+ }
+ }
+
+ // TODO: Cleanup above
+
+ std::vector<AudioDevice> audio_devices;
+ for(size_t i = 0; i < merged_audio_inputs.audio_inputs.size(); ++i) {
+ auto &audio_input = merged_audio_inputs.audio_inputs[i];
+ AVFilterContext *src_ctx = nullptr;
+ if(use_amix)
+ src_ctx = src_filter_ctx[i];
+
+ AudioDevice audio_device;
+ audio_device.audio_input = audio_input;
+ audio_device.src_filter_ctx = src_ctx;
+
+ if(audio_input.name.empty()) {
+ audio_device.sound_device.handle = NULL;
+ audio_device.sound_device.frames = 0;
+ } else {
+ if(sound_device_get_by_name(&audio_device.sound_device, audio_input.name.c_str(), audio_input.description.c_str(), num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) {
+ fprintf(stderr, "Error: failed to get \"%s\" sound device\n", audio_input.name.c_str());
+ _exit(1);
+ }
+ }
+
+ audio_device.frame = create_audio_frame(audio_codec_context);
+ audio_device.frame->pts = 0;
+
+ audio_devices.push_back(std::move(audio_device));
+ }
+
+ AudioTrack audio_track;
+ audio_track.codec_context = audio_codec_context;
+ audio_track.stream = audio_stream;
+ audio_track.audio_devices = std::move(audio_devices);
+ audio_track.graph = graph;
+ audio_track.sink = sink;
+ audio_track.stream_index = audio_stream_index;
+ audio_tracks.push_back(std::move(audio_track));
+ ++audio_stream_index;
}
//av_dump_format(av_format_context, 0, filename, 1);
@@ -1148,351 +2344,359 @@ int main(int argc, char **argv) {
int ret = avio_open(&av_format_context->pb, filename, AVIO_FLAG_WRITE);
if (ret < 0) {
fprintf(stderr, "Error: Could not open '%s': %s\n", filename, av_error_to_string(ret));
- return 1;
+ _exit(1);
}
}
- //video_stream->duration = AV_TIME_BASE * 15;
- //audio_stream->duration = AV_TIME_BASE * 15;
- //av_format_context->duration = AV_TIME_BASE * 15;
if(replay_buffer_size_secs == -1) {
- int ret = avformat_write_header(av_format_context, nullptr);
+ AVDictionary *options = nullptr;
+ av_dict_set(&options, "strict", "experimental", 0);
+ //av_dict_set_int(&av_format_context->metadata, "video_full_range_flag", 1, 0);
+
+ int ret = avformat_write_header(av_format_context, &options);
if (ret < 0) {
fprintf(stderr, "Error occurred when writing header to output file: %s\n", av_error_to_string(ret));
- return 1;
+ _exit(1);
}
- }
-
- // av_frame_free(&rgb_frame);
- // avcodec_close(av_codec_context);
- if(dpy)
- XSelectInput(dpy, src_window_id, StructureNotifyMask | VisibilityChangeMask);
-
- /*
- int damage_event;
- int damage_error;
- if (!XDamageQueryExtension(dpy, &damage_event, &damage_error)) {
- fprintf(stderr, "Error: XDamage is not supported by your X11 server\n");
- return 1;
+ av_dict_free(&options);
}
- Damage damage = XDamageCreate(dpy, src_window_id, XDamageReportNonEmpty);
- XDamageSubtract(dpy, damage,None,None);
- */
-
const double start_time_pts = clock_get_monotonic_seconds();
- CUcontext old_ctx;
- CUarray mapped_array;
- if(src_window_id) {
- res = cuCtxPopCurrent(&old_ctx);
- res = cuCtxPushCurrent(cu_ctx);
-
- // Get texture
- res = cuGraphicsResourceSetMapFlags(
- cuda_graphics_resource, CU_GRAPHICS_MAP_RESOURCE_FLAGS_READ_ONLY);
- res = cuGraphicsMapResources(1, &cuda_graphics_resource, 0);
-
- // Map texture to cuda array
- res = cuGraphicsSubResourceGetMappedArray(&mapped_array,
- cuda_graphics_resource, 0, 0);
- }
-
- // Release texture
- // res = cuGraphicsUnmapResources(1, &cuda_graphics_resource, 0);
-
- double start_time = glfwGetTime();
- double frame_timer_start = start_time;
- double window_resize_timer = start_time;
- bool window_resized = false;
+ double start_time = clock_get_monotonic_seconds();
+ double frame_timer_start = start_time - target_fps; // We want to capture the first frame immediately
int fps_counter = 0;
- int current_fps = 30;
- AVFrame *frame = av_frame_alloc();
- if (!frame) {
- fprintf(stderr, "Error: Failed to allocate frame\n");
- exit(1);
- }
- frame->format = video_codec_context->pix_fmt;
- frame->width = video_codec_context->width;
- frame->height = video_codec_context->height;
-
- if (av_hwframe_get_buffer(video_codec_context->hw_frames_ctx, frame, 0) < 0) {
- fprintf(stderr, "Error: av_hwframe_get_buffer failed\n");
- exit(1);
- }
-
- if(window_pixmap.texture_width < record_width)
- frame->width = window_pixmap.texture_width & ~1;
- else
- frame->width = record_width & ~1;
-
- if(window_pixmap.texture_height < record_height)
- frame->height = window_pixmap.texture_height & ~1;
- else
- frame->height = record_height & ~1;
+ bool paused = false;
+ double paused_time_offset = 0.0;
+ double paused_time_start = 0.0;
std::mutex write_output_mutex;
- std::thread audio_thread;
+ std::mutex audio_filter_mutex;
- double record_start_time = glfwGetTime();
- std::deque<AVPacket> frame_data_queue;
+ const double record_start_time = clock_get_monotonic_seconds();
+ std::deque<std::shared_ptr<PacketData>> frame_data_queue;
bool frames_erased = false;
- SoundDevice sound_device;
- uint8_t *audio_frame_buf;
- if(audio_input_arg.value) {
- if(sound_device_get_by_name(&sound_device, audio_input_arg.value, audio_codec_context->channels, audio_codec_context->frame_size) != 0) {
- fprintf(stderr, "failed to get 'pulse' sound device\n");
- exit(1);
- }
-
- int audio_buffer_size = av_samples_get_buffer_size(NULL, audio_codec_context->channels, audio_codec_context->frame_size, audio_codec_context->sample_fmt, 1);
- audio_frame_buf = (uint8_t *)av_malloc(audio_buffer_size);
- avcodec_fill_audio_frame(audio_frame, audio_codec_context->channels, audio_codec_context->sample_fmt, (const uint8_t*)audio_frame_buf, audio_buffer_size, 1);
-
- audio_thread = std::thread([record_start_time, replay_buffer_size_secs, &frame_data_queue, &frames_erased, audio_codec_context, start_time_pts, fps](AVFormatContext *av_format_context, AVStream *audio_stream, uint8_t *audio_frame_buf, SoundDevice *sound_device, AVFrame *audio_frame, std::mutex *write_output_mutex) mutable {
-
- SwrContext *swr = swr_alloc();
- if(!swr) {
- fprintf(stderr, "Failed to create SwrContext\n");
- exit(1);
- }
- av_opt_set_int(swr, "in_channel_layout", audio_codec_context->channel_layout, 0);
- av_opt_set_int(swr, "out_channel_layout", audio_codec_context->channel_layout, 0);
- av_opt_set_int(swr, "in_sample_rate", audio_codec_context->sample_rate, 0);
- av_opt_set_int(swr, "out_sample_rate", audio_codec_context->sample_rate, 0);
- av_opt_set_sample_fmt(swr, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0);
- av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_FLTP, 0);
- swr_init(swr);
-
- while(running) {
- void *sound_buffer;
- int sound_buffer_size = sound_device_read_next_chunk(sound_device, &sound_buffer);
- if(sound_buffer_size >= 0) {
- // TODO: Instead of converting audio, get float audio from alsa. Or does alsa do conversion internally to get this format?
- swr_convert(swr, &audio_frame_buf, audio_frame->nb_samples, (const uint8_t**)&sound_buffer, sound_buffer_size);
- audio_frame->extended_data = &audio_frame_buf;
- audio_frame->pts = (clock_get_monotonic_seconds() - start_time_pts) * AV_TIME_BASE;
-
- int ret = avcodec_send_frame(audio_codec_context, audio_frame);
- if(ret < 0){
- fprintf(stderr, "Failed to encode!\n");
- break;
+ const size_t audio_buffer_size = 1024 * 4 * 2; // max 4 bytes/sample, 2 channels
+ uint8_t *empty_audio = (uint8_t*)malloc(audio_buffer_size);
+ if(!empty_audio) {
+ fprintf(stderr, "Error: failed to create empty audio\n");
+ _exit(1);
+ }
+ memset(empty_audio, 0, audio_buffer_size);
+
+ const double audio_startup_time_seconds = std::max(0.0, 0.089166 - target_fps);
+
+ for(AudioTrack &audio_track : audio_tracks) {
+ for(AudioDevice &audio_device : audio_track.audio_devices) {
+ audio_device.thread = std::thread([&]() mutable {
+ const AVSampleFormat sound_device_sample_format = audio_format_to_sample_format(audio_codec_context_get_audio_format(audio_track.codec_context));
+ // TODO: Always do conversion for now. This fixes issue with stuttering audio on pulseaudio with opus + multiple audio sources merged
+ const bool needs_audio_conversion = true;//audio_track.codec_context->sample_fmt != sound_device_sample_format;
+ SwrContext *swr = nullptr;
+ if(needs_audio_conversion) {
+ swr = swr_alloc();
+ if(!swr) {
+ fprintf(stderr, "Failed to create SwrContext\n");
+ _exit(1);
}
- if(ret >= 0)
- receive_frames(audio_codec_context, AUDIO_STREAM_INDEX, audio_stream, audio_frame, av_format_context, record_start_time, frame_data_queue, replay_buffer_size_secs, frames_erased, *write_output_mutex);
- } else {
- fprintf(stderr, "failed to read sound from device, error: %d\n", sound_buffer_size);
+ #if LIBAVUTIL_VERSION_MAJOR <= 56
+ av_opt_set_channel_layout(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0);
+ av_opt_set_channel_layout(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0);
+ #else
+ av_opt_set_chlayout(swr, "in_channel_layout", &audio_track.codec_context->ch_layout, 0);
+ av_opt_set_chlayout(swr, "out_channel_layout", &audio_track.codec_context->ch_layout, 0);
+ #endif
+ av_opt_set_int(swr, "in_sample_rate", audio_track.codec_context->sample_rate, 0);
+ av_opt_set_int(swr, "out_sample_rate", audio_track.codec_context->sample_rate, 0);
+ av_opt_set_sample_fmt(swr, "in_sample_fmt", sound_device_sample_format, 0);
+ av_opt_set_sample_fmt(swr, "out_sample_fmt", audio_track.codec_context->sample_fmt, 0);
+ swr_init(swr);
}
- }
- swr_free(&swr);
- }, av_format_context, audio_stream, audio_frame_buf, &sound_device, audio_frame, &write_output_mutex);
- }
+ double received_audio_time = clock_get_monotonic_seconds();
+ const double timeout_sec = 1000.0 / (double)audio_track.codec_context->sample_rate;
+ const int64_t timeout_ms = std::round(timeout_sec * 1000.0);
+
+ while(running) {
+ void *sound_buffer;
+ int sound_buffer_size = -1;
+ //const double time_before_read_seconds = clock_get_monotonic_seconds();
+ if(audio_device.sound_device.handle) {
+ // TODO: use this instead of calculating time to read. But this can fluctuate and we dont want to go back in time,
+ // also it's 0.0 for some users???
+ double latency_seconds = 0.0;
+ sound_buffer_size = sound_device_read_next_chunk(&audio_device.sound_device, &sound_buffer, timeout_sec, &latency_seconds);
+ }
- handle_new_pid_file(replay_buffer_size_secs == -1 ? "record" : "replay");
- started = 1;
+ const bool got_audio_data = sound_buffer_size >= 0;
+ //const double time_after_read_seconds = clock_get_monotonic_seconds();
+ //const double time_to_read_seconds = time_after_read_seconds - time_before_read_seconds;
+ const double this_audio_frame_time = (clock_get_monotonic_seconds() - audio_startup_time_seconds) - paused_time_offset;
- bool redraw = true;
- XEvent e;
- while (running) {
- double frame_start = glfwGetTime();
- glfwPollEvents();
- if(window)
- glClear(GL_COLOR_BUFFER_BIT);
+ if(paused) {
+ if(got_audio_data)
+ received_audio_time = this_audio_frame_time;
- redraw = true;
+ if(!audio_device.sound_device.handle)
+ usleep(timeout_ms * 1000);
- if(src_window_id) {
- if (XCheckTypedWindowEvent(dpy, src_window_id, DestroyNotify, &e)) {
- running = 0;
- }
+ continue;
+ }
- if (XCheckTypedWindowEvent(dpy, src_window_id, VisibilityNotify, &e)) {
- window_resize_timer = glfwGetTime();
- window_resized = true;
- }
+ int ret = av_frame_make_writable(audio_device.frame);
+ if (ret < 0) {
+ fprintf(stderr, "Failed to make audio frame writable\n");
+ break;
+ }
- if (XCheckTypedWindowEvent(dpy, src_window_id, ConfigureNotify, &e) && e.xconfigure.window == src_window_id) {
- // Window resize
- if(e.xconfigure.width != window_width || e.xconfigure.height != window_height) {
- window_width = e.xconfigure.width;
- window_height = e.xconfigure.height;
- window_resize_timer = glfwGetTime();
- window_resized = true;
- }
- }
+ // TODO: Is this |received_audio_time| really correct?
+ const double prev_audio_time = received_audio_time;
+ const double audio_receive_time_diff = this_audio_frame_time - received_audio_time;
+ int64_t num_missing_frames = std::round(audio_receive_time_diff / timeout_sec);
+ if(got_audio_data)
+ num_missing_frames = std::max((int64_t)0, num_missing_frames - 1);
+
+ if(!audio_device.sound_device.handle)
+ num_missing_frames = std::max((int64_t)1, num_missing_frames);
+
+ if(got_audio_data)
+ received_audio_time = this_audio_frame_time;
+
+ // Fucking hell is there a better way to do this? I JUST WANT TO KEEP VIDEO AND AUDIO SYNCED HOLY FUCK I WANT TO KILL MYSELF NOW.
+ // THIS PIECE OF SHIT WANTS EMPTY FRAMES OTHERWISE VIDEO PLAYS TOO FAST TO KEEP UP WITH AUDIO OR THE AUDIO PLAYS TOO EARLY.
+ // BUT WE CANT USE DELAYS TO GIVE DUMMY DATA BECAUSE PULSEAUDIO MIGHT GIVE AUDIO A BIG DELAYED!!!
+ // This garbage is needed because we want to produce constant frame rate videos instead of variable frame rate
+ // videos because bad software such as video editing software and VLC do not support variable frame rate software,
+ // despite nvidia shadowplay and xbox game bar producing variable frame rate videos.
+ // So we have to make sure we produce frames at the same relative rate as the video.
+ if(num_missing_frames >= 5 || !audio_device.sound_device.handle) {
+ // TODO:
+ //audio_track.frame->data[0] = empty_audio;
+ received_audio_time = this_audio_frame_time;
+ if(needs_audio_conversion)
+ swr_convert(swr, &audio_device.frame->data[0], audio_track.codec_context->frame_size, (const uint8_t**)&empty_audio, audio_track.codec_context->frame_size);
+ else
+ audio_device.frame->data[0] = empty_audio;
+
+ // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again
+ std::lock_guard<std::mutex> lock(audio_filter_mutex);
+ for(int i = 0; i < num_missing_frames; ++i) {
+ const int64_t new_pts = ((prev_audio_time - record_start_time) + timeout_sec * i) * AV_TIME_BASE;
+ if(new_pts == audio_device.frame->pts)
+ continue;
+
+ audio_device.frame->pts = new_pts;
+ if(audio_track.graph) {
+ // TODO: av_buffersrc_add_frame
+ if(av_buffersrc_write_frame(audio_device.src_filter_ctx, audio_device.frame) < 0) {
+ fprintf(stderr, "Error: failed to add audio frame to filter\n");
+ }
+ } else {
+ ret = avcodec_send_frame(audio_track.codec_context, audio_device.frame);
+ if(ret >= 0) {
+ // TODO: Move to separate thread because this could write to network (for example when livestreaming)
+ receive_frames(audio_track.codec_context, audio_track.stream_index, audio_track.stream, audio_device.frame->pts, av_format_context, record_start_time, frame_data_queue, replay_buffer_size_secs, frames_erased, write_output_mutex, paused_time_offset);
+ } else {
+ fprintf(stderr, "Failed to encode audio!\n");
+ }
+ }
+ }
+ }
- const double window_resize_timeout = 1.0; // 1 second
- if(window_resized && glfwGetTime() - window_resize_timer >= window_resize_timeout) {
- window_resized = false;
- fprintf(stderr, "Resize window!\n");
- recreate_window_pixmap(dpy, src_window_id, window_pixmap);
- // Resolution must be a multiple of two
- //video_stream->codec->width = window_pixmap.texture_width & ~1;
- //video_stream->codec->height = window_pixmap.texture_height & ~1;
-
- cuGraphicsUnregisterResource(cuda_graphics_resource);
- res = cuGraphicsGLRegisterImage(
- &cuda_graphics_resource, window_pixmap.target_texture_id, GL_TEXTURE_2D,
- CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY);
- if (res != CUDA_SUCCESS) {
- const char *err_str;
- cuGetErrorString(res, &err_str);
- fprintf(stderr,
- "Error: cuGraphicsGLRegisterImage failed, error %s, texture "
- "id: %u\n",
- err_str, window_pixmap.target_texture_id);
- running = false;
- break;
+ if(!audio_device.sound_device.handle)
+ usleep(timeout_ms * 1000);
+
+ if(got_audio_data) {
+ // TODO: Instead of converting audio, get float audio from alsa. Or does alsa do conversion internally to get this format?
+ if(needs_audio_conversion)
+ swr_convert(swr, &audio_device.frame->data[0], audio_track.codec_context->frame_size, (const uint8_t**)&sound_buffer, audio_track.codec_context->frame_size);
+ else
+ audio_device.frame->data[0] = (uint8_t*)sound_buffer;
+
+ const int64_t new_pts = (this_audio_frame_time - record_start_time) * AV_TIME_BASE;
+ if(new_pts != audio_device.frame->pts) {
+ audio_device.frame->pts = new_pts;
+
+ if(audio_track.graph) {
+ std::lock_guard<std::mutex> lock(audio_filter_mutex);
+ // TODO: av_buffersrc_add_frame
+ if(av_buffersrc_write_frame(audio_device.src_filter_ctx, audio_device.frame) < 0) {
+ fprintf(stderr, "Error: failed to add audio frame to filter\n");
+ }
+ } else {
+ ret = avcodec_send_frame(audio_track.codec_context, audio_device.frame);
+ if(ret >= 0) {
+ // TODO: Move to separate thread because this could write to network (for example when livestreaming)
+ receive_frames(audio_track.codec_context, audio_track.stream_index, audio_track.stream, audio_device.frame->pts, av_format_context, record_start_time, frame_data_queue, replay_buffer_size_secs, frames_erased, write_output_mutex, paused_time_offset);
+ } else {
+ fprintf(stderr, "Failed to encode audio!\n");
+ }
+ }
+ }
+ }
}
- res = cuGraphicsResourceSetMapFlags(
- cuda_graphics_resource, CU_GRAPHICS_MAP_RESOURCE_FLAGS_READ_ONLY);
- res = cuGraphicsMapResources(1, &cuda_graphics_resource, 0);
- res = cuGraphicsSubResourceGetMappedArray(&mapped_array, cuda_graphics_resource, 0, 0);
+ if(swr)
+ swr_free(&swr);
+ });
+ }
+ }
- av_frame_free(&frame);
- frame = av_frame_alloc();
- if (!frame) {
- fprintf(stderr, "Error: Failed to allocate frame\n");
- running = false;
- break;
- }
- frame->format = video_codec_context->pix_fmt;
- frame->width = video_codec_context->width;
- frame->height = video_codec_context->height;
+ // Set update_fps to 24 to test if duplicate/delayed frames cause video/audio desync or too fast/slow video.
+ const double update_fps = fps + 190;
+ bool should_stop_error = false;
- if (av_hwframe_get_buffer(video_codec_context->hw_frames_ctx, frame, 0) < 0) {
- fprintf(stderr, "Error: av_hwframe_get_buffer failed\n");
- running = false;
- break;
- }
+ AVFrame *aframe = av_frame_alloc();
- if(window_pixmap.texture_width < record_width)
- frame->width = window_pixmap.texture_width & ~1;
- else
- frame->width = record_width & ~1;
+ int64_t video_pts_counter = 0;
+ int64_t video_prev_pts = 0;
- if(window_pixmap.texture_height < record_height)
- frame->height = window_pixmap.texture_height & ~1;
- else
- frame->height = record_height & ~1;
+ while(running) {
+ double frame_start = clock_get_monotonic_seconds();
- cuMemsetD8((CUdeviceptr)frame->data[0], 0, record_width * record_height * 4);
- }
+ gsr_capture_tick(capture, video_codec_context);
+ should_stop_error = false;
+ if(gsr_capture_should_stop(capture, &should_stop_error)) {
+ running = 0;
+ break;
}
-
++fps_counter;
- double time_now = glfwGetTime();
+ {
+ std::lock_guard<std::mutex> lock(audio_filter_mutex);
+ for(AudioTrack &audio_track : audio_tracks) {
+ if(!audio_track.sink)
+ continue;
+
+ int err = 0;
+ while ((err = av_buffersink_get_frame(audio_track.sink, aframe)) >= 0) {
+ const double this_audio_frame_time = (clock_get_monotonic_seconds() - audio_startup_time_seconds) - paused_time_offset;
+ const int64_t new_pts = (this_audio_frame_time - record_start_time) * AV_TIME_BASE;
+ if(new_pts == aframe->pts)
+ continue;
+ aframe->pts = new_pts;
+ err = avcodec_send_frame(audio_track.codec_context, aframe);
+ if(err >= 0){
+ // TODO: Move to separate thread because this could write to network (for example when livestreaming)
+ receive_frames(audio_track.codec_context, audio_track.stream_index, audio_track.stream, aframe->pts, av_format_context, record_start_time, frame_data_queue, replay_buffer_size_secs, frames_erased, write_output_mutex, paused_time_offset);
+ } else {
+ fprintf(stderr, "Failed to encode audio!\n");
+ }
+ av_frame_unref(aframe);
+ }
+ }
+ }
+
+ double time_now = clock_get_monotonic_seconds();
double frame_timer_elapsed = time_now - frame_timer_start;
double elapsed = time_now - start_time;
if (elapsed >= 1.0) {
- fprintf(stderr, "update fps: %d\n", fps_counter);
+ if(verbose) {
+ fprintf(stderr, "update fps: %d\n", fps_counter);
+ }
start_time = time_now;
- current_fps = fps_counter;
fps_counter = 0;
}
double frame_time_overflow = frame_timer_elapsed - target_fps;
if (frame_time_overflow >= 0.0) {
+ frame_time_overflow = std::min(frame_time_overflow, target_fps);
frame_timer_start = time_now - frame_time_overflow;
- bool frame_captured = true;
- if(redraw) {
- redraw = false;
- if(src_window_id) {
- // TODO: Use a framebuffer instead. glCopyImageSubData requires
- // opengl 4.2
- glCopyImageSubData(
- window_pixmap.texture_id, GL_TEXTURE_2D, 0, 0, 0, 0,
- window_pixmap.target_texture_id, GL_TEXTURE_2D, 0, 0, 0, 0,
- window_pixmap.texture_width, window_pixmap.texture_height, 1);
- int err = glGetError();
- if(err != 0) {
- static bool error_shown = false;
- if(!error_shown) {
- error_shown = true;
- fprintf(stderr, "Error: glCopyImageSubData failed, gl error: %d\n", err);
- }
+ const double this_video_frame_time = clock_get_monotonic_seconds() - paused_time_offset;
+ const int64_t expected_frames = std::round((this_video_frame_time - start_time_pts) / target_fps);
+ const int num_frames = framerate_mode == FramerateMode::CONSTANT ? std::max((int64_t)0LL, expected_frames - video_pts_counter) : 1;
+
+ if(num_frames > 0 && !paused) {
+ gsr_capture_capture(capture, video_frame);
+
+ // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again
+ for(int i = 0; i < num_frames; ++i) {
+ if(framerate_mode == FramerateMode::CONSTANT) {
+ video_frame->pts = video_pts_counter + i;
+ } else {
+ video_frame->pts = (this_video_frame_time - record_start_time) * (double)AV_TIME_BASE;
+ const bool same_pts = video_frame->pts == video_prev_pts;
+ video_prev_pts = video_frame->pts;
+ if(same_pts)
+ continue;
}
- glfwSwapBuffers(window);
- // int err = glGetError();
- // fprintf(stderr, "error: %d\n", err);
-
- // TODO: Remove this copy, which is only possible by using nvenc directly and encoding window_pixmap.target_texture_id
-
- CUDA_MEMCPY2D memcpy_struct;
- memcpy_struct.srcXInBytes = 0;
- memcpy_struct.srcY = 0;
- memcpy_struct.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_ARRAY;
-
- memcpy_struct.dstXInBytes = 0;
- memcpy_struct.dstY = 0;
- memcpy_struct.dstMemoryType = CUmemorytype::CU_MEMORYTYPE_DEVICE;
- memcpy_struct.srcArray = mapped_array;
- memcpy_struct.dstDevice = (CUdeviceptr)frame->data[0];
- memcpy_struct.dstPitch = frame->linesize[0];
- memcpy_struct.WidthInBytes = frame->width * 4;
- memcpy_struct.Height = frame->height;
- cuMemcpy2D(&memcpy_struct);
-
- frame_captured = true;
- } else {
- // TODO: Check when src_cu_device_ptr changes and re-register resource
- uint32_t byte_size = 0;
- CUdeviceptr src_cu_device_ptr = 0;
- frame_captured = nv_fbc_library.capture(&src_cu_device_ptr, &byte_size);
- frame->data[0] = (uint8_t*)src_cu_device_ptr;
+ int ret = avcodec_send_frame(video_codec_context, video_frame);
+ if(ret == 0) {
+ // TODO: Move to separate thread because this could write to network (for example when livestreaming)
+ receive_frames(video_codec_context, VIDEO_STREAM_INDEX, video_stream, video_frame->pts, av_format_context,
+ record_start_time, frame_data_queue, replay_buffer_size_secs, frames_erased, write_output_mutex, paused_time_offset);
+ } else {
+ fprintf(stderr, "Error: avcodec_send_frame failed, error: %s\n", av_error_to_string(ret));
+ }
}
- // res = cuCtxPopCurrent(&old_ctx);
+
+ gsr_capture_end(capture, video_frame);
+ video_pts_counter += num_frames;
}
+ }
- frame->pts = (clock_get_monotonic_seconds() - start_time_pts) * AV_TIME_BASE;
- if (avcodec_send_frame(video_codec_context, frame) >= 0) {
- receive_frames(video_codec_context, VIDEO_STREAM_INDEX, video_stream, frame, av_format_context,
- record_start_time, frame_data_queue, replay_buffer_size_secs, frames_erased, write_output_mutex);
+ if(toggle_pause == 1) {
+ const bool new_paused_state = !paused;
+ if(new_paused_state) {
+ paused_time_start = clock_get_monotonic_seconds();
+ fprintf(stderr, "Paused\n");
} else {
- fprintf(stderr, "Error: avcodec_send_frame failed\n");
+ paused_time_offset += (clock_get_monotonic_seconds() - paused_time_start);
+ fprintf(stderr, "Unpaused\n");
}
+
+ toggle_pause = 0;
+ paused = !paused;
}
if(save_replay_thread.valid() && save_replay_thread.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
save_replay_thread.get();
puts(save_replay_output_filepath.c_str());
- for(size_t i = 0; i < save_replay_packets.size(); ++i) {
- av_packet_unref(&save_replay_packets[i]);
- }
+ fflush(stdout);
+ if(recording_saved_script)
+ run_recording_saved_script_async(recording_saved_script, save_replay_output_filepath.c_str(), "replay");
+ std::lock_guard<std::mutex> lock(write_output_mutex);
save_replay_packets.clear();
}
if(save_replay == 1 && !save_replay_thread.valid() && replay_buffer_size_secs != -1) {
save_replay = 0;
- save_replay_async(video_codec_context, audio_codec_context, VIDEO_STREAM_INDEX, AUDIO_STREAM_INDEX, frame_data_queue, frames_erased, filename, container_format);
+ save_replay_async(video_codec_context, VIDEO_STREAM_INDEX, audio_tracks, frame_data_queue, frames_erased, filename, container_format, file_extension, write_output_mutex, make_folders);
}
- // av_frame_free(&frame);
- double frame_end = glfwGetTime();
- double frame_sleep_fps = 1.0 / 250.0;
+ double frame_end = clock_get_monotonic_seconds();
+ double frame_sleep_fps = 1.0 / update_fps;
double sleep_time = frame_sleep_fps - (frame_end - frame_start);
if(sleep_time > 0.0)
usleep(sleep_time * 1000.0 * 1000.0);
}
- running = 0;
+ running = 0;
- if(save_replay_thread.valid())
+ if(save_replay_thread.valid()) {
save_replay_thread.get();
+ puts(save_replay_output_filepath.c_str());
+ fflush(stdout);
+ if(recording_saved_script)
+ run_recording_saved_script_async(recording_saved_script, save_replay_output_filepath.c_str(), "replay");
+ std::lock_guard<std::mutex> lock(write_output_mutex);
+ save_replay_packets.clear();
+ }
- if(audio_input_arg.value) {
- audio_thread.join();
- sound_device_close(&sound_device);
+ for(AudioTrack &audio_track : audio_tracks) {
+ for(AudioDevice &audio_device : audio_track.audio_devices) {
+ audio_device.thread.join();
+ sound_device_close(&audio_device.sound_device);
+ }
}
+ av_frame_free(&aframe);
+
if (replay_buffer_size_secs == -1 && av_write_trailer(av_format_context) != 0) {
fprintf(stderr, "Failed to write trailer\n");
}
@@ -1500,10 +2704,24 @@ int main(int argc, char **argv) {
if(replay_buffer_size_secs == -1 && !(output_format->flags & AVFMT_NOFILE))
avio_close(av_format_context->pb);
+ gsr_capture_destroy(capture, video_codec_context);
+
+ if(replay_buffer_size_secs == -1 && recording_saved_script)
+ run_recording_saved_script_async(recording_saved_script, filename, "regular");
+
if(dpy) {
- XCompositeUnredirectWindow(dpy, src_window_id, CompositeRedirectAutomatic);
- XCloseDisplay(dpy);
+ // TODO: This causes a crash, why? maybe some other library dlclose xlib and that also happened to unload this???
+ //XCloseDisplay(dpy);
}
- unlink(pid_file);
+ //av_frame_free(&video_frame);
+ free((void*)window_str);
+ free(empty_audio);
+ // We do an _exit here because cuda uses at_exit to do _something_ that causes the program to freeze,
+ // but only on some nvidia driver versions on some gpus (RTX?), and _exit exits the program without calling
+ // the at_exit registered functions.
+ // Cuda (cuvid library in this case) seems to be waiting for a thread that never finishes execution.
+ // Maybe this happens because we dont clean up all ffmpeg resources?
+ // TODO: Investigate this.
+ _exit(should_stop_error ? 3 : 0);
}
diff --git a/src/overclock.c b/src/overclock.c
new file mode 100644
index 0000000..2cba623
--- /dev/null
+++ b/src/overclock.c
@@ -0,0 +1,281 @@
+#include "../include/overclock.h"
+#include <X11/Xlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+// HACK!!!: When a program uses cuda (including nvenc) then the nvidia driver drops to performance level 2 (memory transfer rate is dropped and possibly graphics clock).
+// Nvidia does this because in some very extreme cases of cuda there can be memory corruption when running at max memory transfer rate.
+// So to get around this we overclock memory transfer rate (maybe this should also be done for graphics clock?) to the best performance level while GPU Screen Recorder is running.
+
+// TODO: Does it always drop to performance level 2?
+
+static int min_int(int a, int b) {
+ return a < b ? a : b;
+}
+
+// Fields are 0 if not set
+typedef struct {
+ int perf;
+
+ int nv_clock;
+ int nv_clock_min;
+ int nv_clock_max;
+
+ int mem_clock;
+ int mem_clock_min;
+ int mem_clock_max;
+
+ int mem_transfer_rate;
+ int mem_transfer_rate_min;
+ int mem_transfer_rate_max;
+} NVCTRLPerformanceLevel;
+
+#define MAX_PERFORMANCE_LEVELS 12
+typedef struct {
+ NVCTRLPerformanceLevel performance_level[MAX_PERFORMANCE_LEVELS];
+ int num_performance_levels;
+} NVCTRLPerformanceLevelQuery;
+
+typedef void (*split_callback)(const char *str, size_t size, void *userdata);
+static void split_by_delimiter(const char *str, size_t size, char delimiter, split_callback callback, void *userdata) {
+ const char *it = str;
+ while(it < str + size) {
+ const char *prev_it = it;
+ it = memchr(it, delimiter, (str + size) - it);
+ if(!it)
+ it = str + size;
+
+ callback(prev_it, it - prev_it, userdata);
+ it += 1; // skip delimiter
+ }
+}
+
+typedef enum {
+ NVCTRL_GPU_NVCLOCK,
+ NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE,
+} NvCTRLAttributeType;
+
+static unsigned int attribute_type_to_attribute_param(NvCTRLAttributeType attribute_type) {
+ switch(attribute_type) {
+ case NVCTRL_GPU_NVCLOCK:
+ return NV_CTRL_GPU_NVCLOCK_OFFSET;
+ case NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE:
+ return NV_CTRL_GPU_MEM_TRANSFER_RATE_OFFSET;
+ }
+ return 0;
+}
+
+static unsigned int attribute_type_to_attribute_param_all_levels(NvCTRLAttributeType attribute_type) {
+ switch(attribute_type) {
+ case NVCTRL_GPU_NVCLOCK:
+ return NV_CTRL_GPU_NVCLOCK_OFFSET_ALL_PERFORMANCE_LEVELS;
+ case NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE:
+ return NV_CTRL_GPU_MEM_TRANSFER_RATE_OFFSET_ALL_PERFORMANCE_LEVELS;
+ }
+ return 0;
+}
+
+// Returns 0 on error
+static int xnvctrl_get_attribute_max_value(gsr_xnvctrl *xnvctrl, int num_performance_levels, NvCTRLAttributeType attribute_type) {
+ NVCTRLAttributeValidValuesRec valid;
+ if(xnvctrl->XNVCTRLQueryValidTargetAttributeValues(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, 0, attribute_type_to_attribute_param_all_levels(attribute_type), &valid)) {
+ return valid.u.range.max;
+ }
+
+ if(num_performance_levels > 0 && xnvctrl->XNVCTRLQueryValidTargetAttributeValues(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, num_performance_levels - 1, attribute_type_to_attribute_param(attribute_type), &valid)) {
+ return valid.u.range.max;
+ }
+
+ return 0;
+}
+
+static bool xnvctrl_set_attribute_offset(gsr_xnvctrl *xnvctrl, int num_performance_levels, int offset, NvCTRLAttributeType attribute_type) {
+ bool success = false;
+
+ // NV_CTRL_GPU_MEM_TRANSFER_RATE_OFFSET_ALL_PERFORMANCE_LEVELS works (or at least used to?) without Xorg running as root
+ // so we try that first. NV_CTRL_GPU_MEM_TRANSFER_RATE_OFFSET_ALL_PERFORMANCE_LEVELS also only works with GTX 1000+.
+ // TODO: Reverse engineer NVIDIA Xorg driver so we can set this always without root access.
+ if(xnvctrl->XNVCTRLSetTargetAttributeAndGetStatus(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, 0, attribute_type_to_attribute_param_all_levels(attribute_type), offset))
+ success = true;
+
+ for(int i = 0; i < num_performance_levels; ++i) {
+ success |= xnvctrl->XNVCTRLSetTargetAttributeAndGetStatus(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, i, attribute_type_to_attribute_param(attribute_type), offset);
+ }
+
+ return success;
+}
+
+static void strip(const char **str, int *size) {
+ const char *str_d = *str;
+ int s_d = *size;
+
+ const char *start = str_d;
+ const char *end = start + s_d;
+
+ while(str_d < end) {
+ char c = *str_d;
+ if(c != ' ' && c != '\t' && c != '\n')
+ break;
+ ++str_d;
+ }
+
+ int start_offset = str_d - start;
+ while(s_d > start_offset) {
+ char c = start[s_d];
+ if(c != ' ' && c != '\t' && c != '\n')
+ break;
+ --s_d;
+ }
+
+ *str = str_d;
+ *size = s_d;
+}
+
+static void attribute_callback(const char *str, size_t size, void *userdata) {
+ if(size > 255 - 1)
+ return;
+
+ int size_i = size;
+ strip(&str, &size_i);
+
+ char attribute[255];
+ memcpy(attribute, str, size_i);
+ attribute[size_i] = '\0';
+
+ const char *sep = strchr(attribute, '=');
+ if(!sep)
+ return;
+
+ const char *attribute_name = attribute;
+ size_t attribute_name_len = sep - attribute_name;
+ const char *attribute_value_str = sep + 1;
+
+ int attribute_value = 0;
+ if(sscanf(attribute_value_str, "%d", &attribute_value) != 1)
+ return;
+
+ NVCTRLPerformanceLevel *performance_level = userdata;
+ if(attribute_name_len == 4 && memcmp(attribute_name, "perf", 4) == 0)
+ performance_level->perf = attribute_value;
+ else if(attribute_name_len == 7 && memcmp(attribute_name, "nvclock", 7) == 0)
+ performance_level->nv_clock = attribute_value;
+ else if(attribute_name_len == 10 && memcmp(attribute_name, "nvclockmin", 10) == 0)
+ performance_level->nv_clock_min = attribute_value;
+ else if(attribute_name_len == 10 && memcmp(attribute_name, "nvclockmax", 10) == 0)
+ performance_level->nv_clock_max = attribute_value;
+ else if(attribute_name_len == 8 && memcmp(attribute_name, "memclock", 8) == 0)
+ performance_level->mem_clock = attribute_value;
+ else if(attribute_name_len == 11 && memcmp(attribute_name, "memclockmin", 11) == 0)
+ performance_level->mem_clock_min = attribute_value;
+ else if(attribute_name_len == 11 && memcmp(attribute_name, "memclockmax", 11) == 0)
+ performance_level->mem_clock_max = attribute_value;
+ else if(attribute_name_len == 15 && memcmp(attribute_name, "memTransferRate", 15) == 0)
+ performance_level->mem_transfer_rate = attribute_value;
+ else if(attribute_name_len == 18 && memcmp(attribute_name, "memTransferRatemin", 18) == 0)
+ performance_level->mem_transfer_rate_min = attribute_value;
+ else if(attribute_name_len == 18 && memcmp(attribute_name, "memTransferRatemax", 18) == 0)
+ performance_level->mem_transfer_rate_max = attribute_value;
+}
+
+static void attribute_line_callback(const char *str, size_t size, void *userdata) {
+ NVCTRLPerformanceLevelQuery *query = userdata;
+ if(query->num_performance_levels >= MAX_PERFORMANCE_LEVELS)
+ return;
+
+ NVCTRLPerformanceLevel *current_performance_level = &query->performance_level[query->num_performance_levels];
+ memset(current_performance_level, 0, sizeof(NVCTRLPerformanceLevel));
+ ++query->num_performance_levels;
+ split_by_delimiter(str, size, ',', attribute_callback, current_performance_level);
+}
+
+static bool xnvctrl_get_performance_levels(gsr_xnvctrl *xnvctrl, NVCTRLPerformanceLevelQuery *query) {
+ bool success = false;
+ memset(query, 0, sizeof(NVCTRLPerformanceLevelQuery));
+
+ char *attributes = NULL;
+ if(!xnvctrl->XNVCTRLQueryTargetStringAttribute(xnvctrl->display, NV_CTRL_TARGET_TYPE_GPU, 0, 0, NV_CTRL_STRING_PERFORMANCE_MODES, &attributes)) {
+ success = false;
+ goto done;
+ }
+
+ split_by_delimiter(attributes, strlen(attributes), ';', attribute_line_callback, query);
+ success = true;
+
+ done:
+ if(attributes)
+ XFree(attributes);
+
+ return success;
+}
+
+static int compare_mem_transfer_rate_max_asc(const void *a, const void *b) {
+ const NVCTRLPerformanceLevel *perf_a = a;
+ const NVCTRLPerformanceLevel *perf_b = b;
+ return perf_a->mem_transfer_rate_max - perf_b->mem_transfer_rate_max;
+}
+
+bool gsr_overclock_load(gsr_overclock *self, Display *display) {
+ memset(self, 0, sizeof(gsr_overclock));
+ self->num_performance_levels = 0;
+
+ return gsr_xnvctrl_load(&self->xnvctrl, display);
+}
+
+void gsr_overclock_unload(gsr_overclock *self) {
+ gsr_xnvctrl_unload(&self->xnvctrl);
+}
+
+bool gsr_overclock_start(gsr_overclock *self) {
+ int basep = 0;
+ int errorp = 0;
+ if(!self->xnvctrl.XNVCTRLQueryExtension(self->xnvctrl.display, &basep, &errorp)) {
+ fprintf(stderr, "gsr warning: gsr_overclock_start: xnvctrl is not supported on your system, failed to overclock memory transfer rate\n");
+ return false;
+ }
+
+ NVCTRLPerformanceLevelQuery query;
+ if(!xnvctrl_get_performance_levels(&self->xnvctrl, &query) || query.num_performance_levels == 0) {
+ fprintf(stderr, "gsr warning: gsr_overclock_start: failed to get performance levels for overclocking\n");
+ return false;
+ }
+ self->num_performance_levels = query.num_performance_levels;
+
+ qsort(query.performance_level, query.num_performance_levels, sizeof(NVCTRLPerformanceLevel), compare_mem_transfer_rate_max_asc);
+
+ int target_transfer_rate_offset = xnvctrl_get_attribute_max_value(&self->xnvctrl, query.num_performance_levels, NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE);
+ if(query.num_performance_levels > 1) {
+ const int transfer_rate_max_diff = query.performance_level[query.num_performance_levels - 1].mem_transfer_rate_max - query.performance_level[query.num_performance_levels - 2].mem_transfer_rate_max;
+ target_transfer_rate_offset = min_int(target_transfer_rate_offset, transfer_rate_max_diff);
+ if(target_transfer_rate_offset >= 0 && xnvctrl_set_attribute_offset(&self->xnvctrl, self->num_performance_levels, target_transfer_rate_offset, NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE)) {
+ fprintf(stderr, "gsr info: gsr_overclock_start: sucessfully set memory transfer rate offset to %d\n", target_transfer_rate_offset);
+ } else {
+ fprintf(stderr, "gsr info: gsr_overclock_start: failed to overclock memory transfer rate offset to %d\n", target_transfer_rate_offset);
+ }
+ }
+
+ // TODO: Sort by nv_clock_max
+
+ // TODO: Enable. Crashes on my system (gtx 1080) so it's disabled for now. Seems to crash even if graphics clock is increasd by 1, let alone 1200
+ /*
+ int target_nv_clock_offset = xnvctrl_get_attribute_max_value(&self->xnvctrl, query.num_performance_levels, NVCTRL_GPU_NVCLOCK);
+ if(query.num_performance_levels > 1) {
+ const int nv_clock_max_diff = query.performance_level[query.num_performance_levels - 1].nv_clock_max - query.performance_level[query.num_performance_levels - 2].nv_clock_max;
+ target_nv_clock_offset = min_int(target_nv_clock_offset, nv_clock_max_diff);
+ if(target_nv_clock_offset >= 0 && xnvctrl_set_attribute_offset(&self->xnvctrl, self->num_performance_levels, target_nv_clock_offset, NVCTRL_GPU_NVCLOCK)) {
+ fprintf(stderr, "gsr info: gsr_overclock_start: sucessfully set nv clock offset to %d\n", target_nv_clock_offset);
+ } else {
+ fprintf(stderr, "gsr info: gsr_overclock_start: failed to overclock nv clock offset to %d\n", target_nv_clock_offset);
+ }
+ }
+ */
+
+ XSync(self->xnvctrl.display, False);
+ return true;
+}
+
+void gsr_overclock_stop(gsr_overclock *self) {
+ xnvctrl_set_attribute_offset(&self->xnvctrl, self->num_performance_levels, 0, NVCTRL_ATTRIB_GPU_MEM_TRANSFER_RATE);
+ //xnvctrl_set_attribute_offset(&self->xnvctrl, self->num_performance_levels, 0, NVCTRL_GPU_NVCLOCK);
+ XSync(self->xnvctrl.display, False);
+}
diff --git a/src/shader.c b/src/shader.c
new file mode 100644
index 0000000..dcb956b
--- /dev/null
+++ b/src/shader.c
@@ -0,0 +1,143 @@
+#include "../include/shader.h"
+#include "../include/egl.h"
+#include <stdio.h>
+#include <assert.h>
+
+static int min_int(int a, int b) {
+ return a < b ? a : b;
+}
+
+static unsigned int loader_shader(gsr_egl *egl, unsigned int type, const char *source) {
+ unsigned int shader_id = egl->glCreateShader(type);
+ if(shader_id == 0) {
+ fprintf(stderr, "gsr error: loader_shader: failed to create shader, error: %d\n", egl->glGetError());
+ return 0;
+ }
+
+ egl->glShaderSource(shader_id, 1, &source, NULL);
+ egl->glCompileShader(shader_id);
+
+ int compiled = 0;
+ egl->glGetShaderiv(shader_id, GL_COMPILE_STATUS, &compiled);
+ if(!compiled) {
+ int info_length = 0;
+ egl->glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &info_length);
+
+ if(info_length > 1) {
+ char info_log[4096];
+ egl->glGetShaderInfoLog(shader_id, min_int(4096, info_length), NULL, info_log);
+ fprintf(stderr, "gsr error: loader shader: failed to compile shader, error:\n%s\nshader source:\n%s\n", info_log, source);
+ }
+
+ egl->glDeleteShader(shader_id);
+ return 0;
+ }
+
+ return shader_id;
+}
+
+static unsigned int load_program(gsr_egl *egl, const char *vertex_shader, const char *fragment_shader) {
+ unsigned int vertex_shader_id = 0;
+ unsigned int fragment_shader_id = 0;
+ unsigned int program_id = 0;
+ int linked = 0;
+
+ if(vertex_shader) {
+ vertex_shader_id = loader_shader(egl, GL_VERTEX_SHADER, vertex_shader);
+ if(vertex_shader_id == 0)
+ goto err;
+ }
+
+ if(fragment_shader) {
+ fragment_shader_id = loader_shader(egl, GL_FRAGMENT_SHADER, fragment_shader);
+ if(fragment_shader_id == 0)
+ goto err;
+ }
+
+ program_id = egl->glCreateProgram();
+ if(program_id == 0) {
+ fprintf(stderr, "gsr error: load_program: failed to create shader program, error: %d\n", egl->glGetError());
+ goto err;
+ }
+
+ if(vertex_shader_id)
+ egl->glAttachShader(program_id, vertex_shader_id);
+
+ if(fragment_shader_id)
+ egl->glAttachShader(program_id, fragment_shader_id);
+
+ egl->glLinkProgram(program_id);
+
+ egl->glGetProgramiv(program_id, GL_LINK_STATUS, &linked);
+ if(!linked) {
+ int info_length = 0;
+ egl->glGetProgramiv(program_id, GL_INFO_LOG_LENGTH, &info_length);
+
+ if(info_length > 1) {
+ char info_log[4096];
+ egl->glGetProgramInfoLog(program_id, min_int(4096, info_length), NULL, info_log);
+ fprintf(stderr, "gsr error: load program: linking shader program failed, error:\n%s\n", info_log);
+ }
+
+ goto err;
+ }
+
+ if(fragment_shader_id)
+ egl->glDeleteShader(fragment_shader_id);
+ if(vertex_shader_id)
+ egl->glDeleteShader(vertex_shader_id);
+
+ return program_id;
+
+ err:
+ if(program_id)
+ egl->glDeleteProgram(program_id);
+ if(fragment_shader_id)
+ egl->glDeleteShader(fragment_shader_id);
+ if(vertex_shader_id)
+ egl->glDeleteShader(vertex_shader_id);
+ return 0;
+}
+
+int gsr_shader_init(gsr_shader *self, gsr_egl *egl, const char *vertex_shader, const char *fragment_shader) {
+ assert(egl);
+ self->egl = egl;
+ self->program_id = 0;
+
+ if(!vertex_shader && !fragment_shader) {
+ fprintf(stderr, "gsr error: gsr_shader_init: vertex shader and fragment shader can't be NULL at the same time\n");
+ return -1;
+ }
+
+ self->program_id = load_program(self->egl, vertex_shader, fragment_shader);
+ if(self->program_id == 0)
+ return -1;
+
+ return 0;
+}
+
+void gsr_shader_deinit(gsr_shader *self) {
+ if(!self->egl)
+ return;
+
+ if(self->program_id) {
+ self->egl->glDeleteProgram(self->program_id);
+ self->program_id = 0;
+ }
+
+ self->egl = NULL;
+}
+
+int gsr_shader_bind_attribute_location(gsr_shader *self, const char *attribute, int location) {
+ while(self->egl->glGetError()) {}
+ self->egl->glBindAttribLocation(self->program_id, location, attribute);
+ return self->egl->glGetError();
+}
+
+void gsr_shader_use(gsr_shader *self) {
+ self->egl->glUseProgram(self->program_id);
+}
+
+void gsr_shader_use_none(gsr_shader *self) {
+ self->egl->glUseProgram(0);
+}
diff --git a/src/sound.cpp b/src/sound.cpp
index 779aa87..53000bd 100644
--- a/src/sound.cpp
+++ b/src/sound.cpp
@@ -1,154 +1,375 @@
-/*
- Copyright (C) 2020 dec05eba
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
#include "../include/sound.hpp"
+extern "C" {
+#include "../include/utils.h"
+}
#include <stdlib.h>
#include <stdio.h>
+#include <string.h>
+#include <cmath>
+#include <time.h>
-#ifdef PULSEAUDIO
-#include <pulse/simple.h>
+#include <pulse/pulseaudio.h>
+#include <pulse/mainloop.h>
+#include <pulse/xmalloc.h>
#include <pulse/error.h>
-int sound_device_get_by_name(SoundDevice *device, const char *name, unsigned int num_channels, unsigned int period_frame_size) {
- pa_sample_spec ss;
- ss.format = PA_SAMPLE_S16LE;
- ss.rate = 48000;
- ss.channels = num_channels;
- int error;
+#define CHECK_DEAD_GOTO(p, rerror, label) \
+ do { \
+ if (!(p)->context || !PA_CONTEXT_IS_GOOD(pa_context_get_state((p)->context)) || \
+ !(p)->stream || !PA_STREAM_IS_GOOD(pa_stream_get_state((p)->stream))) { \
+ if (((p)->context && pa_context_get_state((p)->context) == PA_CONTEXT_FAILED) || \
+ ((p)->stream && pa_stream_get_state((p)->stream) == PA_STREAM_FAILED)) { \
+ if (rerror) \
+ *(rerror) = pa_context_errno((p)->context); \
+ } else \
+ if (rerror) \
+ *(rerror) = PA_ERR_BADSTATE; \
+ goto label; \
+ } \
+ } while(false);
- pa_simple *pa_handle = pa_simple_new(nullptr, "gpu-screen-recorder", PA_STREAM_RECORD, name, "record", &ss, nullptr, nullptr, &error);
- if(!pa_handle) {
- fprintf(stderr, "pa_simple_new() failed: %s. Audio input device %s might not be valid\n", pa_strerror(error), name);
- return -1;
+struct pa_handle {
+ pa_context *context;
+ pa_stream *stream;
+ pa_mainloop *mainloop;
+
+ const void *read_data;
+ size_t read_index, read_length;
+
+ uint8_t *output_data;
+ size_t output_index, output_length;
+
+ int operation_success;
+ double latency_seconds;
+};
+
+static void pa_sound_device_free(pa_handle *s) {
+ assert(s);
+
+ if (s->stream)
+ pa_stream_unref(s->stream);
+
+ if (s->context) {
+ pa_context_disconnect(s->context);
+ pa_context_unref(s->context);
+ }
+
+ if (s->mainloop)
+ pa_mainloop_free(s->mainloop);
+
+ if (s->output_data) {
+ free(s->output_data);
+ s->output_data = NULL;
}
- int buffer_size = period_frame_size * 2 * num_channels; // 2 bytes/sample, @num_channels channels
+ pa_xfree(s);
+}
+
+static pa_handle* pa_sound_device_new(const char *server,
+ const char *name,
+ const char *dev,
+ const char *stream_name,
+ const pa_sample_spec *ss,
+ const pa_buffer_attr *attr,
+ int *rerror) {
+ pa_handle *p;
+ int error = PA_ERR_INTERNAL, r;
+
+ p = pa_xnew0(pa_handle, 1);
+ p->read_data = NULL;
+ p->read_length = 0;
+ p->read_index = 0;
+ p->latency_seconds = 0.0;
+
+ const int buffer_size = attr->fragsize;
void *buffer = malloc(buffer_size);
if(!buffer) {
fprintf(stderr, "failed to allocate buffer for audio\n");
- pa_simple_free(pa_handle);
- return -1;
+ *rerror = -1;
+ return NULL;
}
- fprintf(stderr, "Using pulseaudio\n");
+ p->output_data = (uint8_t*)buffer;
+ p->output_length = buffer_size;
+ p->output_index = 0;
- device->handle = pa_handle;
- device->buffer = buffer;
- device->buffer_size = buffer_size;
- device->frames = period_frame_size;
- return 0;
-}
+ if (!(p->mainloop = pa_mainloop_new()))
+ goto fail;
-void sound_device_close(SoundDevice *device) {
- pa_simple_free((pa_simple*)device->handle);
- free(device->buffer);
+ if (!(p->context = pa_context_new(pa_mainloop_get_api(p->mainloop), name)))
+ goto fail;
+
+ if (pa_context_connect(p->context, server, PA_CONTEXT_NOFLAGS, NULL) < 0) {
+ error = pa_context_errno(p->context);
+ goto fail;
+ }
+
+ for (;;) {
+ pa_context_state_t state = pa_context_get_state(p->context);
+
+ if (state == PA_CONTEXT_READY)
+ break;
+
+ if (!PA_CONTEXT_IS_GOOD(state)) {
+ error = pa_context_errno(p->context);
+ goto fail;
+ }
+
+ pa_mainloop_iterate(p->mainloop, 1, NULL);
+ }
+
+ if (!(p->stream = pa_stream_new(p->context, stream_name, ss, NULL))) {
+ error = pa_context_errno(p->context);
+ goto fail;
+ }
+
+ r = pa_stream_connect_record(p->stream, dev, attr,
+ (pa_stream_flags_t)(PA_STREAM_INTERPOLATE_TIMING|PA_STREAM_ADJUST_LATENCY|PA_STREAM_AUTO_TIMING_UPDATE));
+
+ if (r < 0) {
+ error = pa_context_errno(p->context);
+ goto fail;
+ }
+
+ for (;;) {
+ pa_stream_state_t state = pa_stream_get_state(p->stream);
+
+ if (state == PA_STREAM_READY)
+ break;
+
+ if (!PA_STREAM_IS_GOOD(state)) {
+ error = pa_context_errno(p->context);
+ goto fail;
+ }
+
+ pa_mainloop_iterate(p->mainloop, 1, NULL);
+ }
+
+ return p;
+
+fail:
+ if (rerror)
+ *rerror = error;
+ pa_sound_device_free(p);
+ return NULL;
}
-int sound_device_read_next_chunk(SoundDevice *device, void **buffer) {
- int error;
- if(pa_simple_read((pa_simple*)device->handle, device->buffer, device->buffer_size, &error) < 0) {
- fprintf(stderr, "pa_simple_read() failed: %s\n", pa_strerror(error));
- return -1;
+static int pa_sound_device_read(pa_handle *p, double timeout_seconds) {
+ assert(p);
+
+ const double start_time = clock_get_monotonic_seconds();
+
+ bool success = false;
+ int r = 0;
+ int *rerror = &r;
+ pa_usec_t latency = 0;
+ int negative = 0;
+
+ CHECK_DEAD_GOTO(p, rerror, fail);
+
+ while (p->output_index < p->output_length) {
+ if(clock_get_monotonic_seconds() - start_time >= timeout_seconds)
+ return -1;
+
+ if(!p->read_data) {
+ pa_mainloop_prepare(p->mainloop, 1 * 1000); // 1 ms
+ pa_mainloop_poll(p->mainloop);
+ pa_mainloop_dispatch(p->mainloop);
+
+ if(pa_stream_peek(p->stream, &p->read_data, &p->read_length) < 0)
+ goto fail;
+
+ if(!p->read_data && p->read_length == 0)
+ continue;
+
+ if(!p->read_data && p->read_length > 0) {
+ // There is a hole in the stream :( drop it. Maybe we should generate silence instead? TODO
+ if(pa_stream_drop(p->stream) != 0)
+ goto fail;
+ continue;
+ }
+
+ if(p->read_length <= 0) {
+ p->read_data = NULL;
+ if(pa_stream_drop(p->stream) != 0)
+ goto fail;
+
+ CHECK_DEAD_GOTO(p, rerror, fail);
+ continue;
+ }
+
+ pa_operation_unref(pa_stream_update_timing_info(p->stream, NULL, NULL));
+ // TODO: Deal with one pa_stream_peek not being enough. In that case we need to add multiple of these together(?)
+ if(pa_stream_get_latency(p->stream, &latency, &negative) >= 0) {
+ p->latency_seconds = negative ? -(double)latency : latency;
+ if(p->latency_seconds < 0.0)
+ p->latency_seconds = 0.0;
+ p->latency_seconds *= 0.0000001;
+ }
+ }
+
+ const size_t space_free_in_output_buffer = p->output_length - p->output_index;
+ if(space_free_in_output_buffer < p->read_length) {
+ memcpy(p->output_data + p->output_index, (const uint8_t*)p->read_data + p->read_index, space_free_in_output_buffer);
+ p->output_index = 0;
+ p->read_index += space_free_in_output_buffer;
+ p->read_length -= space_free_in_output_buffer;
+ break;
+ } else {
+ memcpy(p->output_data + p->output_index, (const uint8_t*)p->read_data + p->read_index, p->read_length);
+ p->output_index += p->read_length;
+ p->read_data = NULL;
+ p->read_length = 0;
+ p->read_index = 0;
+
+ if(pa_stream_drop(p->stream) != 0)
+ goto fail;
+
+ if(p->output_index == p->output_length) {
+ p->output_index = 0;
+ break;
+ }
+ }
}
- *buffer = device->buffer;
- return device->frames;
+
+ success = true;
+
+ fail:
+ return success ? 0 : -1;
}
-#else
-#define ALSA_PCM_NEW_HW_PARAMS_API
-#include <alsa/asoundlib.h>
-
-int sound_device_get_by_name(SoundDevice *device, const char *name, unsigned int num_channels, unsigned int period_frame_size) {
- int rc;
- snd_pcm_t *handle;
-
- rc = snd_pcm_open(&handle, name, SND_PCM_STREAM_CAPTURE, 0);
- if(rc < 0) {
- fprintf(stderr, "unable to open pcm device 'default', reason: %s\n", snd_strerror(rc));
- return rc;
+
+static pa_sample_format_t audio_format_to_pulse_audio_format(AudioFormat audio_format) {
+ switch(audio_format) {
+ case S16: return PA_SAMPLE_S16LE;
+ case S32: return PA_SAMPLE_S32LE;
+ case F32: return PA_SAMPLE_FLOAT32LE;
}
+ assert(false);
+ return PA_SAMPLE_S16LE;
+}
- snd_pcm_hw_params_t *params;
- snd_pcm_hw_params_alloca(&params);
- // Fill the params with default values
- snd_pcm_hw_params_any(handle, params);
- // Interleaved mode
- snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED);
- // Signed 16--bit little-endian format
- snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE);
- snd_pcm_hw_params_set_channels(handle, params, num_channels);
-
- // 48000 bits/second samling rate (DVD quality)
- unsigned int val = 48000;
- int dir;
- snd_pcm_hw_params_set_rate_near(handle, params, &val, &dir);
-
- snd_pcm_uframes_t frames = period_frame_size;
- snd_pcm_hw_params_set_period_size_near(handle, params, &frames, &dir);
-
- // Write the parmeters to the driver
- rc = snd_pcm_hw_params(handle, params);
- if(rc < 0) {
- fprintf(stderr, "unable to set hw parameters, reason: %s\n", snd_strerror(rc));
- snd_pcm_close(handle);
- return rc;
+static int audio_format_to_get_bytes_per_sample(AudioFormat audio_format) {
+ switch(audio_format) {
+ case S16: return 2;
+ case S32: return 4;
+ case F32: return 4;
}
+ assert(false);
+ return 2;
+}
- // Use a buffer large enough to hold one period
- snd_pcm_hw_params_get_period_size(params, &frames, &dir);
- int buffer_size = frames * 2 * num_channels; // 2 bytes/sample, @num_channels channels
- void *buffer = malloc(buffer_size);
- if(!buffer) {
- fprintf(stderr, "failed to allocate buffer for audio\n");
- snd_pcm_close(handle);
+int sound_device_get_by_name(SoundDevice *device, const char *device_name, const char *description, unsigned int num_channels, unsigned int period_frame_size, AudioFormat audio_format) {
+ pa_sample_spec ss;
+ ss.format = audio_format_to_pulse_audio_format(audio_format);
+ ss.rate = 48000;
+ ss.channels = num_channels;
+
+ pa_buffer_attr buffer_attr;
+ buffer_attr.fragsize = period_frame_size * audio_format_to_get_bytes_per_sample(audio_format) * num_channels; // 2/4 bytes/sample, @num_channels channels
+ buffer_attr.tlength = -1;
+ buffer_attr.prebuf = -1;
+ buffer_attr.minreq = -1;
+ buffer_attr.maxlength = buffer_attr.fragsize;
+
+ int error = 0;
+ pa_handle *handle = pa_sound_device_new(nullptr, description, device_name, description, &ss, &buffer_attr, &error);
+ if(!handle) {
+ fprintf(stderr, "pa_sound_device_new() failed: %s. Audio input device %s might not be valid\n", pa_strerror(error), description);
return -1;
}
- fprintf(stderr, "Using alsa\n");
-
device->handle = handle;
- device->buffer = buffer;
- device->buffer_size = buffer_size;
- device->frames = frames;
+ device->frames = period_frame_size;
return 0;
}
void sound_device_close(SoundDevice *device) {
- /* TODO: Is this also needed in @sound_device_get_by_name on failure? */
- // TODO: This has been commented out since it causes the thread to block forever. Why?
- //snd_pcm_drain((snd_pcm_t*)device->handle);
- snd_pcm_close((snd_pcm_t*)device->handle);
- free(device->buffer);
+ if(device->handle)
+ pa_sound_device_free((pa_handle*)device->handle);
+ device->handle = NULL;
+}
+
+int sound_device_read_next_chunk(SoundDevice *device, void **buffer, double timeout_sec, double *latency_seconds) {
+ pa_handle *pa = (pa_handle*)device->handle;
+ if(pa_sound_device_read(pa, timeout_sec) < 0) {
+ //fprintf(stderr, "pa_simple_read() failed: %s\n", pa_strerror(error));
+ *latency_seconds = 0.0;
+ return -1;
+ }
+ *buffer = pa->output_data;
+ *latency_seconds = pa->latency_seconds;
+ return device->frames;
}
-int sound_device_read_next_chunk(SoundDevice *device, void **buffer) {
- int rc = snd_pcm_readi((snd_pcm_t*)device->handle, device->buffer, device->frames);
- if (rc == -EPIPE) {
- /* overrun */
- fprintf(stderr, "overrun occured\n");
- snd_pcm_prepare((snd_pcm_t*)device->handle);
- return rc;
- } else if(rc < 0) {
- fprintf(stderr, "failed to read from sound device, reason: %s\n", snd_strerror(rc));
- return rc;
- } else if (rc != (int)device->frames) {
- fprintf(stderr, "short read, read %d frames\n", rc);
+static void pa_state_cb(pa_context *c, void *userdata) {
+ pa_context_state state = pa_context_get_state(c);
+ int *pa_ready = (int*)userdata;
+ switch(state) {
+ case PA_CONTEXT_UNCONNECTED:
+ case PA_CONTEXT_CONNECTING:
+ case PA_CONTEXT_AUTHORIZING:
+ case PA_CONTEXT_SETTING_NAME:
+ default:
+ break;
+ case PA_CONTEXT_FAILED:
+ case PA_CONTEXT_TERMINATED:
+ *pa_ready = 2;
+ break;
+ case PA_CONTEXT_READY:
+ *pa_ready = 1;
+ break;
}
- *buffer = device->buffer;
- return rc;
}
-#endif
+
+static void pa_sourcelist_cb(pa_context *ctx, const pa_source_info *source_info, int eol, void *userdata) {
+ (void)ctx;
+ if(eol > 0)
+ return;
+
+ std::vector<AudioInput> *inputs = (std::vector<AudioInput>*)userdata;
+ inputs->push_back({ source_info->name, source_info->description });
+}
+
+std::vector<AudioInput> get_pulseaudio_inputs() {
+ std::vector<AudioInput> inputs;
+ pa_mainloop *main_loop = pa_mainloop_new();
+
+ pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder");
+ pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL);
+ int state = 0;
+ int pa_ready = 0;
+ pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready);
+
+ pa_operation *pa_op = NULL;
+
+ for(;;) {
+ // Not ready
+ if(pa_ready == 0) {
+ pa_mainloop_iterate(main_loop, 1, NULL);
+ continue;
+ }
+
+ switch(state) {
+ case 0: {
+ pa_op = pa_context_get_source_info_list(ctx, pa_sourcelist_cb, &inputs);
+ ++state;
+ break;
+ }
+ }
+
+ // Couldn't get connection to the server
+ if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) {
+ if(pa_op)
+ pa_operation_unref(pa_op);
+ pa_context_disconnect(ctx);
+ pa_context_unref(ctx);
+ break;
+ }
+
+ pa_mainloop_iterate(main_loop, 1, NULL);
+ }
+
+ pa_mainloop_free(main_loop);
+ return inputs;
+}
diff --git a/src/utils.c b/src/utils.c
new file mode 100644
index 0000000..7dd1890
--- /dev/null
+++ b/src/utils.c
@@ -0,0 +1,450 @@
+#include "../include/utils.h"
+#include <time.h>
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <xf86drmMode.h>
+#include <xf86drm.h>
+#include <stdlib.h>
+#include <X11/Xatom.h>
+
+double clock_get_monotonic_seconds(void) {
+ struct timespec ts;
+ ts.tv_sec = 0;
+ ts.tv_nsec = 0;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (double)ts.tv_sec + (double)ts.tv_nsec * 0.000000001;
+}
+
+static const XRRModeInfo* get_mode_info(const XRRScreenResources *sr, RRMode id) {
+ for(int i = 0; i < sr->nmode; ++i) {
+ if(sr->modes[i].id == id)
+ return &sr->modes[i];
+ }
+ return NULL;
+}
+
+static gsr_monitor_rotation x11_rotation_to_gsr_rotation(int rot) {
+ switch(rot) {
+ case RR_Rotate_0: return GSR_MONITOR_ROT_0;
+ case RR_Rotate_90: return GSR_MONITOR_ROT_90;
+ case RR_Rotate_180: return GSR_MONITOR_ROT_180;
+ case RR_Rotate_270: return GSR_MONITOR_ROT_270;
+ }
+ return GSR_MONITOR_ROT_0;
+}
+
+static gsr_monitor_rotation wayland_transform_to_gsr_rotation(int32_t rot) {
+ switch(rot) {
+ case 0: return GSR_MONITOR_ROT_0;
+ case 1: return GSR_MONITOR_ROT_90;
+ case 2: return GSR_MONITOR_ROT_180;
+ case 3: return GSR_MONITOR_ROT_270;
+ }
+ return GSR_MONITOR_ROT_0;
+}
+
+static uint32_t x11_output_get_connector_id(Display *dpy, RROutput output, Atom randr_connector_id_atom) {
+ Atom type = 0;
+ int format = 0;
+ unsigned long bytes_after = 0;
+ unsigned long nitems = 0;
+ unsigned char *prop = NULL;
+ XRRGetOutputProperty(dpy, output, randr_connector_id_atom, 0, 128, false, false, AnyPropertyType, &type, &format, &nitems, &bytes_after, &prop);
+
+ long result = 0;
+ if(type == XA_INTEGER && format == 32)
+ result = *(long*)prop;
+
+ free(prop);
+ return result;
+}
+
+void for_each_active_monitor_output_x11(Display *display, active_monitor_callback callback, void *userdata) {
+ XRRScreenResources *screen_res = XRRGetScreenResources(display, DefaultRootWindow(display));
+ if(!screen_res)
+ return;
+
+ const Atom randr_connector_id_atom = XInternAtom(display, "CONNECTOR_ID", False);
+
+ char display_name[256];
+ for(int i = 0; i < screen_res->noutput; ++i) {
+ XRROutputInfo *out_info = XRRGetOutputInfo(display, screen_res, screen_res->outputs[i]);
+ if(out_info && out_info->crtc && out_info->connection == RR_Connected) {
+ XRRCrtcInfo *crt_info = XRRGetCrtcInfo(display, screen_res, out_info->crtc);
+ if(crt_info && crt_info->mode) {
+ const XRRModeInfo *mode_info = get_mode_info(screen_res, crt_info->mode);
+ if(mode_info && out_info->nameLen < (int)sizeof(display_name)) {
+ memcpy(display_name, out_info->name, out_info->nameLen);
+ display_name[out_info->nameLen] = '\0';
+
+ const gsr_monitor monitor = {
+ .name = display_name,
+ .name_len = out_info->nameLen,
+ .pos = { .x = crt_info->x, .y = crt_info->y },
+ .size = { .x = (int)crt_info->width, .y = (int)crt_info->height },
+ .crt_info = crt_info,
+ .connector_id = x11_output_get_connector_id(display, screen_res->outputs[i], randr_connector_id_atom),
+ .rotation = x11_rotation_to_gsr_rotation(crt_info->rotation),
+ .monitor_identifier = 0
+ };
+ callback(&monitor, userdata);
+ }
+ }
+ if(crt_info)
+ XRRFreeCrtcInfo(crt_info);
+ }
+ if(out_info)
+ XRRFreeOutputInfo(out_info);
+ }
+
+ XRRFreeScreenResources(screen_res);
+}
+
+typedef struct {
+ int type;
+ int count;
+ int count_active;
+} drm_connector_type_count;
+
+#define CONNECTOR_TYPE_COUNTS 32
+
+static drm_connector_type_count* drm_connector_types_get_index(drm_connector_type_count *type_counts, int *num_type_counts, int connector_type) {
+ for(int i = 0; i < *num_type_counts; ++i) {
+ if(type_counts[i].type == connector_type)
+ return &type_counts[i];
+ }
+
+ if(*num_type_counts == CONNECTOR_TYPE_COUNTS)
+ return NULL;
+
+ const int index = *num_type_counts;
+ type_counts[index].type = connector_type;
+ type_counts[index].count = 0;
+ type_counts[index].count_active = 0;
+ ++*num_type_counts;
+ return &type_counts[index];
+}
+
+static bool connector_get_property_by_name(int drmfd, drmModeConnectorPtr props, const char *name, uint64_t *result) {
+ for(int i = 0; i < props->count_props; ++i) {
+ drmModePropertyPtr prop = drmModeGetProperty(drmfd, props->props[i]);
+ if(prop) {
+ if(strcmp(name, prop->name) == 0) {
+ *result = props->prop_values[i];
+ drmModeFreeProperty(prop);
+ return true;
+ }
+ drmModeFreeProperty(prop);
+ }
+ }
+ return false;
+}
+
+/* TODO: Support more connector types*/
+static int get_connector_type_by_name(const char *name) {
+ int len = strlen(name);
+ if(len >= 5 && strncmp(name, "HDMI-", 5) == 0)
+ return 1;
+ else if(len >= 3 && strncmp(name, "DP-", 3) == 0)
+ return 2;
+ else if(len >= 12 && strncmp(name, "DisplayPort-", 12) == 0)
+ return 3;
+ else if(len >= 4 && strncmp(name, "eDP-", 4) == 0)
+ return 4;
+ else
+ return -1;
+}
+
+static uint32_t monitor_identifier_from_type_and_count(int monitor_type_index, int monitor_type_count) {
+ return ((uint32_t)monitor_type_index << 16) | ((uint32_t)monitor_type_count);
+}
+
+static void for_each_active_monitor_output_wayland(const gsr_egl *egl, active_monitor_callback callback, void *userdata) {
+ drm_connector_type_count type_counts[CONNECTOR_TYPE_COUNTS];
+ int num_type_counts = 0;
+
+ for(int i = 0; i < egl->wayland.num_outputs; ++i) {
+ const gsr_wayland_output *output = &egl->wayland.outputs[i];
+ if(!output->name)
+ continue;
+
+ const int connector_type_index = get_connector_type_by_name(output->name);
+ drm_connector_type_count *connector_type = NULL;
+ if(connector_type_index != -1)
+ connector_type = drm_connector_types_get_index(type_counts, &num_type_counts, connector_type_index);
+
+ if(connector_type) {
+ ++connector_type->count;
+ ++connector_type->count_active;
+ }
+
+ const gsr_monitor monitor = {
+ .name = output->name,
+ .name_len = strlen(output->name),
+ .pos = { .x = output->pos.x, .y = output->pos.y },
+ .size = { .x = output->size.x, .y = output->size.y },
+ .crt_info = NULL,
+ .connector_id = 0,
+ .rotation = wayland_transform_to_gsr_rotation(output->transform),
+ .monitor_identifier = connector_type ? monitor_identifier_from_type_and_count(connector_type_index, connector_type->count_active) : 0
+ };
+ callback(&monitor, userdata);
+ }
+}
+
+static void for_each_active_monitor_output_drm(const gsr_egl *egl, active_monitor_callback callback, void *userdata) {
+ int fd = open(egl->card_path, O_RDONLY);
+ if(fd == -1)
+ return;
+
+ drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, 1);
+
+ drm_connector_type_count type_counts[CONNECTOR_TYPE_COUNTS];
+ int num_type_counts = 0;
+
+ char display_name[256];
+ drmModeResPtr resources = drmModeGetResources(fd);
+ if(resources) {
+ for(int i = 0; i < resources->count_connectors; ++i) {
+ drmModeConnectorPtr connector = drmModeGetConnectorCurrent(fd, resources->connectors[i]);
+ if(!connector)
+ continue;
+
+ drm_connector_type_count *connector_type = drm_connector_types_get_index(type_counts, &num_type_counts, connector->connector_type);
+ const char *connection_name = drmModeGetConnectorTypeName(connector->connector_type);
+ const int connection_name_len = strlen(connection_name);
+ if(connector_type)
+ ++connector_type->count;
+
+ if(connector->connection != DRM_MODE_CONNECTED) {
+ drmModeFreeConnector(connector);
+ continue;
+ }
+
+ if(connector_type)
+ ++connector_type->count_active;
+
+ uint64_t crtc_id = 0;
+ connector_get_property_by_name(fd, connector, "CRTC_ID", &crtc_id);
+
+ drmModeCrtcPtr crtc = drmModeGetCrtc(fd, crtc_id);
+ if(connector_type && crtc_id > 0 && crtc && connection_name_len + 5 < (int)sizeof(display_name)) {
+ const int display_name_len = snprintf(display_name, sizeof(display_name), "%s-%d", connection_name, connector_type->count);
+ const int connector_type_index_name = get_connector_type_by_name(display_name);
+ const gsr_monitor monitor = {
+ .name = display_name,
+ .name_len = display_name_len,
+ .pos = { .x = crtc->x, .y = crtc->y },
+ .size = { .x = (int)crtc->width, .y = (int)crtc->height },
+ .crt_info = NULL,
+ .connector_id = connector->connector_id,
+ .rotation = GSR_MONITOR_ROT_0,
+ .monitor_identifier = connector_type_index_name != -1 ? monitor_identifier_from_type_and_count(connector_type_index_name, connector_type->count_active) : 0
+ };
+ callback(&monitor, userdata);
+ }
+
+ if(crtc)
+ drmModeFreeCrtc(crtc);
+
+ drmModeFreeConnector(connector);
+ }
+ drmModeFreeResources(resources);
+ }
+
+ close(fd);
+}
+
+void for_each_active_monitor_output(const gsr_egl *egl, gsr_connection_type connection_type, active_monitor_callback callback, void *userdata) {
+ switch(connection_type) {
+ case GSR_CONNECTION_X11:
+ for_each_active_monitor_output_x11(egl->x11.dpy, callback, userdata);
+ break;
+ case GSR_CONNECTION_WAYLAND:
+ for_each_active_monitor_output_wayland(egl, callback, userdata);
+ break;
+ case GSR_CONNECTION_DRM:
+ for_each_active_monitor_output_drm(egl, callback, userdata);
+ break;
+ }
+}
+
+static void get_monitor_by_name_callback(const gsr_monitor *monitor, void *userdata) {
+ get_monitor_by_name_userdata *data = (get_monitor_by_name_userdata*)userdata;
+ if(!data->found_monitor && strcmp(data->name, monitor->name) == 0) {
+ data->monitor->pos = monitor->pos;
+ data->monitor->size = monitor->size;
+ data->monitor->connector_id = monitor->connector_id;
+ data->monitor->rotation = monitor->rotation;
+ data->monitor->monitor_identifier = monitor->monitor_identifier;
+ data->found_monitor = true;
+ }
+}
+
+bool get_monitor_by_name(const gsr_egl *egl, gsr_connection_type connection_type, const char *name, gsr_monitor *monitor) {
+ get_monitor_by_name_userdata userdata;
+ userdata.name = name;
+ userdata.name_len = strlen(name);
+ userdata.monitor = monitor;
+ userdata.found_monitor = false;
+ for_each_active_monitor_output(egl, connection_type, get_monitor_by_name_callback, &userdata);
+ return userdata.found_monitor;
+}
+
+typedef struct {
+ const gsr_monitor *monitor;
+ gsr_monitor_rotation rotation;
+} get_monitor_by_connector_id_userdata;
+
+static void get_monitor_by_connector_id_callback(const gsr_monitor *monitor, void *userdata) {
+ get_monitor_by_connector_id_userdata *data = (get_monitor_by_connector_id_userdata*)userdata;
+ if(monitor->connector_id == data->monitor->connector_id ||
+ (!monitor->connector_id && monitor->monitor_identifier == data->monitor->monitor_identifier))
+ {
+ data->rotation = monitor->rotation;
+ }
+}
+
+gsr_monitor_rotation drm_monitor_get_display_server_rotation(const gsr_egl *egl, const gsr_monitor *monitor) {
+ if(egl->wayland.dpy) {
+ get_monitor_by_connector_id_userdata userdata;
+ userdata.monitor = monitor;
+ userdata.rotation = GSR_MONITOR_ROT_0;
+ for_each_active_monitor_output_wayland(egl, get_monitor_by_connector_id_callback, &userdata);
+ return userdata.rotation;
+ } else {
+ get_monitor_by_connector_id_userdata userdata;
+ userdata.monitor = monitor;
+ userdata.rotation = GSR_MONITOR_ROT_0;
+ for_each_active_monitor_output_x11(egl->x11.dpy, get_monitor_by_connector_id_callback, &userdata);
+ return userdata.rotation;
+ }
+
+ return GSR_MONITOR_ROT_0;
+}
+
+bool gl_get_gpu_info(gsr_egl *egl, gsr_gpu_info *info) {
+ const char *software_renderers[] = { "llvmpipe", "SWR", "softpipe", NULL };
+ bool supported = true;
+ const unsigned char *gl_vendor = egl->glGetString(GL_VENDOR);
+ const unsigned char *gl_renderer = egl->glGetString(GL_RENDERER);
+
+ info->gpu_version = 0;
+
+ if(!gl_vendor) {
+ fprintf(stderr, "gsr error: failed to get gpu vendor\n");
+ supported = false;
+ goto end;
+ }
+
+ if(gl_renderer) {
+ for(int i = 0; software_renderers[i]; ++i) {
+ if(strstr((const char*)gl_renderer, software_renderers[i])) {
+ fprintf(stderr, "gsr error: your opengl environment is not properly setup. It's using %s (software rendering) for opengl instead of your graphics card. Please make sure your graphics driver is properly installed\n", software_renderers[i]);
+ supported = false;
+ goto end;
+ }
+ }
+ }
+
+ if(strstr((const char*)gl_vendor, "AMD"))
+ info->vendor = GSR_GPU_VENDOR_AMD;
+ else if(strstr((const char*)gl_vendor, "Intel"))
+ info->vendor = GSR_GPU_VENDOR_INTEL;
+ else if(strstr((const char*)gl_vendor, "NVIDIA"))
+ info->vendor = GSR_GPU_VENDOR_NVIDIA;
+ else {
+ fprintf(stderr, "gsr error: unknown gpu vendor: %s\n", gl_vendor);
+ supported = false;
+ goto end;
+ }
+
+ if(gl_renderer) {
+ if(info->vendor == GSR_GPU_VENDOR_NVIDIA)
+ sscanf((const char*)gl_renderer, "%*s %*s %*s %d", &info->gpu_version);
+ }
+
+ end:
+ return supported;
+}
+
+static bool try_card_has_valid_plane(const char *card_path) {
+ drmVersion *ver = NULL;
+ drmModePlaneResPtr planes = NULL;
+ bool found_screen_card = false;
+
+ int fd = open(card_path, O_RDONLY);
+ if(fd == -1)
+ return false;
+
+ ver = drmGetVersion(fd);
+ if(!ver || strstr(ver->name, "nouveau"))
+ goto next;
+
+ drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
+
+ planes = drmModeGetPlaneResources(fd);
+ if(!planes)
+ goto next;
+
+ for(uint32_t j = 0; j < planes->count_planes; ++j) {
+ drmModePlanePtr plane = drmModeGetPlane(fd, planes->planes[j]);
+ if(!plane)
+ continue;
+
+ if(plane->fb_id)
+ found_screen_card = true;
+
+ drmModeFreePlane(plane);
+ if(found_screen_card)
+ break;
+ }
+
+ next:
+ if(planes)
+ drmModeFreePlaneResources(planes);
+ if(ver)
+ drmFreeVersion(ver);
+ close(fd);
+ if(found_screen_card)
+ return true;
+
+ return false;
+}
+
+bool gsr_get_valid_card_path(gsr_egl *egl, char *output) {
+ if(egl->dri_card_path) {
+ strncpy(output, egl->dri_card_path, 127);
+ return try_card_has_valid_plane(output);
+ }
+
+ for(int i = 0; i < 10; ++i) {
+ snprintf(output, 127, DRM_DEV_NAME, DRM_DIR_NAME, i);
+ if(try_card_has_valid_plane(output))
+ return true;
+ }
+ return false;
+}
+
+bool gsr_card_path_get_render_path(const char *card_path, char *render_path) {
+ int fd = open(card_path, O_RDONLY);
+ if(fd == -1)
+ return false;
+
+ char *render_path_tmp = drmGetRenderDeviceNameFromFd(fd);
+ if(render_path_tmp) {
+ strncpy(render_path, render_path_tmp, 127);
+ free(render_path_tmp);
+ close(fd);
+ return true;
+ }
+
+ close(fd);
+ return false;
+}
+
+int even_number_ceil(int value) {
+ return value + (value & 1);
+}
diff --git a/src/window_texture.c b/src/window_texture.c
new file mode 100644
index 0000000..0f4aa2c
--- /dev/null
+++ b/src/window_texture.c
@@ -0,0 +1,123 @@
+#include "../include/window_texture.h"
+#include <X11/extensions/Xcomposite.h>
+
+static int x11_supports_composite_named_window_pixmap(Display *display) {
+ int extension_major;
+ int extension_minor;
+ if(!XCompositeQueryExtension(display, &extension_major, &extension_minor))
+ return 0;
+
+ int major_version;
+ int minor_version;
+ return XCompositeQueryVersion(display, &major_version, &minor_version) && (major_version > 0 || minor_version >= 2);
+}
+
+int window_texture_init(WindowTexture *window_texture, Display *display, Window window, gsr_egl *egl) {
+ window_texture->display = display;
+ window_texture->window = window;
+ window_texture->pixmap = None;
+ window_texture->texture_id = 0;
+ window_texture->redirected = 0;
+ window_texture->egl = egl;
+
+ if(!x11_supports_composite_named_window_pixmap(display))
+ return 1;
+
+ XCompositeRedirectWindow(display, window, CompositeRedirectAutomatic);
+ window_texture->redirected = 1;
+ return window_texture_on_resize(window_texture);
+}
+
+static void window_texture_cleanup(WindowTexture *self, int delete_texture) {
+ if(delete_texture && self->texture_id) {
+ self->egl->glDeleteTextures(1, &self->texture_id);
+ self->texture_id = 0;
+ }
+
+ if(self->pixmap) {
+ XFreePixmap(self->display, self->pixmap);
+ self->pixmap = None;
+ }
+}
+
+void window_texture_deinit(WindowTexture *self) {
+ if(self->redirected) {
+ XCompositeUnredirectWindow(self->display, self->window, CompositeRedirectAutomatic);
+ self->redirected = 0;
+ }
+ window_texture_cleanup(self, 1);
+}
+
+int window_texture_on_resize(WindowTexture *self) {
+ window_texture_cleanup(self, 0);
+
+ int result = 0;
+ Pixmap pixmap = None;
+ unsigned int texture_id = 0;
+ EGLImage image = NULL;
+
+ const intptr_t pixmap_attrs[] = {
+ EGL_IMAGE_PRESERVED_KHR, EGL_TRUE,
+ EGL_NONE,
+ };
+
+ pixmap = XCompositeNameWindowPixmap(self->display, self->window);
+ if(!pixmap) {
+ result = 2;
+ goto cleanup;
+ }
+
+ if(self->texture_id == 0) {
+ self->egl->glGenTextures(1, &texture_id);
+ if(texture_id == 0) {
+ result = 3;
+ goto cleanup;
+ }
+ self->egl->glBindTexture(GL_TEXTURE_2D, texture_id);
+ } else {
+ self->egl->glBindTexture(GL_TEXTURE_2D, self->texture_id);
+ texture_id = self->texture_id;
+ }
+
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ self->egl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+
+ while(self->egl->glGetError()) {}
+ while(self->egl->eglGetError() != EGL_SUCCESS) {}
+
+ image = self->egl->eglCreateImage(self->egl->egl_display, NULL, EGL_NATIVE_PIXMAP_KHR, (EGLClientBuffer)pixmap, pixmap_attrs);
+ if(!image) {
+ result = 4;
+ goto cleanup;
+ }
+
+ self->egl->glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image);
+ if(self->egl->glGetError() != 0 || self->egl->eglGetError() != EGL_SUCCESS) {
+ result = 5;
+ goto cleanup;
+ }
+
+ self->pixmap = pixmap;
+ self->texture_id = texture_id;
+
+ cleanup:
+ self->egl->glBindTexture(GL_TEXTURE_2D, 0);
+
+ if(image)
+ self->egl->eglDestroyImage(self->egl->egl_display, image);
+
+ if(result != 0) {
+ if(texture_id != 0)
+ self->egl->glDeleteTextures(1, &texture_id);
+ if(pixmap)
+ XFreePixmap(self->display, pixmap);
+ }
+
+ return result;
+}
+
+unsigned int window_texture_get_opengl_texture_id(WindowTexture *self) {
+ return self->texture_id;
+}
diff --git a/src/xnvctrl.c b/src/xnvctrl.c
new file mode 100644
index 0000000..b738455
--- /dev/null
+++ b/src/xnvctrl.c
@@ -0,0 +1,46 @@
+#include "../include/xnvctrl.h"
+#include "../include/library_loader.h"
+#include <string.h>
+#include <stdio.h>
+#include <dlfcn.h>
+
+bool gsr_xnvctrl_load(gsr_xnvctrl *self, Display *display) {
+ memset(self, 0, sizeof(gsr_xnvctrl));
+ self->display = display;
+
+ dlerror(); /* clear */
+ void *lib = dlopen("libXNVCtrl.so.0", RTLD_LAZY);
+ if(!lib) {
+ fprintf(stderr, "gsr error: gsr_xnvctrl_load failed: failed to load libXNVCtrl.so.0, error: %s\n", dlerror());
+ return false;
+ }
+
+ dlsym_assign required_dlsym[] = {
+ { (void**)&self->XNVCTRLQueryExtension, "XNVCTRLQueryExtension" },
+ { (void**)&self->XNVCTRLSetTargetAttributeAndGetStatus, "XNVCTRLSetTargetAttributeAndGetStatus" },
+ { (void**)&self->XNVCTRLQueryValidTargetAttributeValues, "XNVCTRLQueryValidTargetAttributeValues" },
+ { (void**)&self->XNVCTRLQueryTargetStringAttribute, "XNVCTRLQueryTargetStringAttribute" },
+
+ { NULL, NULL }
+ };
+
+ if(!dlsym_load_list(lib, required_dlsym)) {
+ fprintf(stderr, "gsr error: gsr_xnvctrl_load failed: missing required symbols in libXNVCtrl.so.0\n");
+ goto fail;
+ }
+
+ self->library = lib;
+ return true;
+
+ fail:
+ dlclose(lib);
+ memset(self, 0, sizeof(gsr_xnvctrl));
+ return false;
+}
+
+void gsr_xnvctrl_unload(gsr_xnvctrl *self) {
+ if(self->library) {
+ dlclose(self->library);
+ memset(self, 0, sizeof(gsr_xnvctrl));
+ }
+}
diff --git a/study/color_space_transform_matrix.png b/study/color_space_transform_matrix.png
new file mode 100644
index 0000000..2b7729e5
--- /dev/null
+++ b/study/color_space_transform_matrix.png
Binary files differ
diff --git a/study/create_matrix.py b/study/create_matrix.py
new file mode 100755
index 0000000..1599a12
--- /dev/null
+++ b/study/create_matrix.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+
+import sys
+
+def usage():
+ print("usage: Kr Kg Kb full|limited")
+ print("examples:")
+ print(" create_matrix.py 0.2126 0.7152 0.0722 full")
+ print(" create_matrix.py 0.2126 0.7152 0.0722 limited")
+ exit(1)
+
+def a(v):
+ if v >= 0:
+ return " %f" % v
+ else:
+ return "%f" % v
+
+def main(argv):
+ if len(argv) != 5:
+ usage()
+
+ Kr = float(sys.argv[1])
+ Kg = float(sys.argv[2])
+ Kb = float(sys.argv[3])
+ color_range = sys.argv[4]
+ luma_offset = 0.0
+ transform_range = 1.0
+
+ if color_range == "full":
+ pass
+ elif color_range == "limited":
+ transform_range = (235.0 - 16.0) / 255.0
+ luma_offset = 16.0 / 255.0
+
+ matrix = [
+ [Kr, Kg, Kb],
+ [-0.5 * (Kr / (1.0 - Kb)), -0.5 * (Kg / (1.0 - Kb)), 0.5],
+ [0.5, -0.5 * (Kg / (1.0 - Kr)), -0.5 * (Kb / (1.0 -Kr))],
+ [0.0, 0.5, 0.5]
+ ]
+
+ # Transform from row major to column major for glsl
+ print("const mat4 RGBtoYUV = mat4(%f, %s, %s, %f," % (matrix[0][0] * transform_range, a(matrix[1][0] * transform_range), a(matrix[2][0] * transform_range), 0.0))
+ print(" %f, %s, %s, %f," % (matrix[0][1] * transform_range, a(matrix[1][1] * transform_range), a(matrix[2][1] * transform_range), 0.0))
+ print(" %f, %s, %s, %f," % (matrix[0][2] * transform_range, a(matrix[1][2] * transform_range), a(matrix[2][2] * transform_range), 0.0))
+ print(" %f, %s, %s, %f);" % (matrix[3][0] + luma_offset, a(matrix[3][1]), a(matrix[3][2]), 1.0))
+
+main(sys.argv)
diff --git a/uninstall.sh b/uninstall.sh
new file mode 100755
index 0000000..9457d1f
--- /dev/null
+++ b/uninstall.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+[ $(id -u) -ne 0 ] && echo "You need root privileges to run the uninstall script" && exit 1
+
+rm -f "/usr/bin/gsr-kms-server"
+rm -f "/usr/bin/gpu-screen-recorder"
+rm -f "/usr/lib/systemd/user/gpu-screen-recorder.service"
+
+echo "Successfully uninstalled gpu-screen-recorder" \ No newline at end of file