├── .clang-format ├── .clang-tidy ├── .gitignore ├── .versioning ├── current └── version-update ├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake ├── CPPCheck.cmake ├── EmbeddedGLSLTarget.cmake ├── FindDRM.cmake ├── FindFFMpeg.cmake ├── FindPipewire.cmake └── MakeTest.cmake ├── doc ├── architecture.md ├── data │ └── ghostrunner.mp4.metrics ├── faq.md ├── images │ ├── ghostrunner-wayland-detailed-audio.png │ ├── ghostrunner-wayland-detailed-video.png │ ├── ghostrunner-wayland-smoothed.png │ ├── ghostrunner-x11-detailed-audio.png │ ├── ghostrunner-x11-detailed-video.png │ └── ghostrunner-x11-smoothed.png └── performance.md ├── src ├── CMakeLists.txt ├── av.hpp ├── av │ ├── buffer.cpp │ ├── buffer.hpp │ ├── buffer_pool.cpp │ ├── buffer_pool.hpp │ ├── codec.cpp │ ├── codec.hpp │ ├── format.cpp │ ├── format.hpp │ ├── frame.cpp │ ├── frame.hpp │ ├── fwd.hpp │ ├── media_chunk.cpp │ ├── media_chunk.hpp │ ├── packet.cpp │ ├── packet.hpp │ ├── sample_format.cpp │ └── sample_format.hpp ├── config.hpp.in ├── display.hpp ├── display │ ├── display.cpp │ └── display.hpp ├── drm.hpp ├── drm │ ├── messaging.cpp │ ├── messaging.hpp │ ├── planes.cpp │ └── planes.hpp ├── error.cpp ├── error.hpp ├── gl │ ├── buffer.cpp │ ├── buffer.hpp │ ├── core.cpp │ ├── core.hpp │ ├── error.cpp │ ├── error.hpp │ ├── framebuffer.cpp │ ├── framebuffer.hpp │ ├── gl.hpp │ ├── object.hpp │ ├── program.cpp │ ├── program.hpp │ ├── shader.cpp │ ├── shader.hpp │ ├── texture.cpp │ ├── texture.hpp │ ├── vertex_array_object.cpp │ └── vertex_array_object.hpp ├── glsl │ ├── CMakeLists.txt │ ├── default_fragment.glsl │ ├── default_vertex.glsl │ ├── mouse_fragment.glsl │ └── mouse_vertex.glsl ├── handlers.hpp ├── handlers │ ├── audio_chunk_writer.cpp │ ├── audio_chunk_writer.hpp │ ├── drm_video_frame_writer.cpp │ ├── drm_video_frame_writer.hpp │ ├── stream_finalizer.cpp │ ├── stream_finalizer.hpp │ ├── video_frame_writer.cpp │ └── video_frame_writer.hpp ├── io.hpp ├── io │ ├── accept_handler.cpp │ ├── accept_handler.hpp │ ├── message_handler.hpp │ ├── message_receiver.cpp │ ├── message_receiver.hpp │ ├── message_sender.cpp │ ├── message_sender.hpp │ ├── process.cpp │ ├── process.hpp │ ├── signals.cpp │ ├── signals.hpp │ ├── unix_socket.cpp │ └── unix_socket.hpp ├── kms.cpp ├── logging.cpp ├── logging.hpp ├── main.cpp ├── metrics │ ├── formatting.hpp │ ├── histogram.hpp │ ├── metrics.cpp │ └── metrics.hpp ├── nvidia.cpp ├── nvidia.hpp ├── nvidia │ ├── NvFBC.h │ └── cuda.hpp ├── platform.hpp ├── platform │ ├── egl.cpp │ ├── egl.hpp │ ├── opengl.cpp │ ├── opengl.hpp │ ├── wayland.cpp │ └── wayland.hpp ├── services.hpp ├── services │ ├── audio_service.cpp │ ├── audio_service.hpp │ ├── color_converter.cpp │ ├── color_converter.hpp │ ├── context.cpp │ ├── context.hpp │ ├── drm_video_service.cpp │ ├── drm_video_service.hpp │ ├── encoder.cpp │ ├── encoder.hpp │ ├── encoder_service.cpp │ ├── encoder_service.hpp │ ├── readiness.cpp │ ├── readiness.hpp │ ├── service.cpp │ ├── service.hpp │ ├── service_registry.cpp │ ├── service_registry.hpp │ ├── signal_service.cpp │ ├── signal_service.hpp │ ├── video_service.cpp │ └── video_service.hpp ├── shadow_cast.hpp ├── utils.hpp └── utils │ ├── base64.cpp │ ├── base64.hpp │ ├── borrowed_ptr.hpp │ ├── cmd_line.cpp │ ├── cmd_line.hpp │ ├── contracts.cpp │ ├── contracts.hpp │ ├── elapsed.cpp │ ├── elapsed.hpp │ ├── frame_time.cpp │ ├── frame_time.hpp │ ├── intrusive_list.hpp │ ├── non_pointer.hpp │ ├── pool.hpp │ ├── receiver.hpp │ ├── result.cpp │ ├── result.hpp │ ├── scope_guard.hpp │ └── symbol.hpp ├── tests ├── CMakeLists.txt ├── base64_tests.cpp ├── cmd_line_tests.cpp ├── data │ └── cyberpunk-girl-rgb-640x640.rgb ├── gl_buffer_tests.cpp ├── gl_error_tests.cpp ├── gl_render_to_texture_test.cpp ├── gl_shader_tests.cpp ├── gl_texture_tests.cpp ├── glsl │ ├── CMakeLists.txt │ ├── flipped_y_vertex.glsl │ ├── identity_fragment.glsl │ ├── textured_brightness_fragment.glsl │ └── textured_vertex.glsl ├── histogram_tests.cpp ├── intrusive_list_tests.cpp ├── nvidia_tests.cpp ├── pool_tests.cpp ├── testing.cpp └── testing.hpp └── tools ├── add-version-change ├── install-helper.sh.in ├── make-dist ├── metrics ├── average ├── percentile └── plot ├── next-version ├── performance-report └── rollup-version-changes /.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | Checks: 'clang-diagnostic-*,clang-analyzer-*' 3 | WarningsAsErrors: '' 4 | HeaderFilterRegex: '' 5 | AnalyzeTemporaryDtors: false 6 | FormatStyle: none 7 | User: greg 8 | CheckOptions: 9 | - key: llvm-else-after-return.WarnOnConditionVariables 10 | value: '0' 11 | - key: modernize-loop-convert.MinConfidence 12 | value: reasonable 13 | - key: modernize-replace-auto-ptr.IncludeStyle 14 | value: llvm 15 | - key: cert-str34-c.DiagnoseSignedUnsignedCharComparisons 16 | value: '0' 17 | - key: google-readability-namespace-comments.ShortNamespaceLines 18 | value: '10' 19 | - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField 20 | value: '0' 21 | - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic 22 | value: '1' 23 | - key: cert-dcl16-c.NewSuffixes 24 | value: 'L;LL;LU;LLU' 25 | - key: google-readability-braces-around-statements.ShortStatementLines 26 | value: '1' 27 | - key: modernize-pass-by-value.IncludeStyle 28 | value: llvm 29 | - key: google-readability-namespace-comments.SpacesBeforeComments 30 | value: '2' 31 | - key: modernize-loop-convert.MaxCopySize 32 | value: '16' 33 | - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors 34 | value: '1' 35 | - key: modernize-use-nullptr.NullMacros 36 | value: 'NULL' 37 | - key: llvm-qualified-auto.AddConstToQualified 38 | value: '0' 39 | - key: modernize-loop-convert.NamingStyle 40 | value: CamelCase 41 | - key: llvm-else-after-return.WarnOnUnfixable 42 | value: '0' 43 | - key: google-readability-function-size.StatementThreshold 44 | value: '800' 45 | ... 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .cache/ 3 | .private/ 4 | experimental/ 5 | dist/ 6 | report/ 7 | -------------------------------------------------------------------------------- /.versioning/current: -------------------------------------------------------------------------------- 1 | 0.7.2 2 | -------------------------------------------------------------------------------- /.versioning/version-update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | changesfile=$(mktemp "/tmp/$(basename $0)-${$}.XXXXXX") 5 | trap "rm -rf ${changesfile}" INT TERM EXIT 6 | 7 | ( 8 | echo "## $NEXT_VERSION" 9 | for changetype in MAJOR MINOR PATCH; do 10 | if egrep '^'${changetype}' ' $CHANGES_FILE >/dev/null; then 11 | echo "### ${changetype} Changes:" 12 | egrep '^'${changetype}' ' $CHANGES_FILE | while read _patch line; do 13 | echo "- ${line}" 14 | done 15 | echo 16 | fi 17 | done 18 | ) >${changesfile} 19 | 20 | if [ -f "./CHANGELOG.md" ]; then 21 | cat "./CHANGELOG.md" >>${changesfile} 22 | fi 23 | 24 | cp "${changesfile}" "./CHANGELOG.md" 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.7.2 2 | ### PATCH Changes: 3 | - The mouse cursor is now captured on Wayland (fixes #14) 4 | 5 | ## 0.7.1 6 | ### PATCH Changes: 7 | - Users now are now no longer required to have the `shadow-cast-kms` executable in their `$PATH` if installed to a custom location. The process will look for it in the same location as the main `shadow-cast` executable. 8 | - Reverts to using FPS as the video encoding timebase. This avoids some surprising behaviour with nvenc 9 | - Fixes color conversion on Wayland compositors that don't use a linear pixel format for DRM planes (#39) (thanks: @SleepingPanda) 10 | - Fixes an issue where a DRM driver can be incorretly reported as unavailable in Wayland (thanks: @SleepingPanda). 11 | - Adds ability to output audio and video frame-time histogram metrics. This is enabled via the configure-time option `-DSHADOW_CAST_ENABLE_HISTOGRAMS=ON` 12 | - Fixes an A/V desync issue with the opus encoder 13 | - Fixes an issue where builds would fail if the FFMpeg headers are located under a subfolder within `/usr/include` (thanks: @SleepingPanda) 14 | 15 | ## 0.7.0 16 | ### MINOR Changes: 17 | - Improves encoding performance by using a dedicated thread 18 | - Adds a build-time option to enable frame time metric collection 19 | - Reduces frame "jitter" when capturing certain games in X11 20 | - Enables LTO when supported by compiler/linker 21 | 22 | ### PATCH Changes: 23 | - Fixes an issue that was causing builds to fail on musl platforms (#24) 24 | - Fixes a memory leak in H/W encoding pipeline 25 | 26 | ## 0.6.3 27 | ### PATCH Changes: 28 | - Fixes an issue where the build would fail if `cppcheck` executable wasn't available 29 | 30 | ## 0.6.2 31 | ### PATCH Changes: 32 | - Fixes a bug that caused no valid values to be accepted for the `-V` cmdline option (#18) 33 | - Fixes build incompatibilities with ffmpeg6 - thankyou, @guihkx (#17) 34 | 35 | ## 0.6.1 36 | ### PATCH Changes: 37 | - Fixes bug where capture would fail with `ERROR: No DRM planes received` on some Wayland compositors ([#12](https://github.com/gmbeard/shadow-cast/issues/12)) 38 | 39 | ## 0.6.0 40 | ### MINOR Changes: 41 | - Adds Wayland support 42 | 43 | ## 0.5.0 44 | ### MINOR Changes: 45 | - Removes excess `AVPacket` allocations 46 | - Adds command line options 47 | - Allows video/audio codecs, audio sample rate, and video frame rate to be specified on cmdline 48 | 49 | ## 0.4.0 50 | ### MINOR Changes: 51 | - Removes unnecessary thread synchronization to reduce contention 52 | - Reads the screen resolution from the default X display 53 | 54 | ## 0.3.0 55 | ### MINOR Changes: 56 | - Disables NvFBC push model by default to prevent some capture artifacts 57 | - Performance improvement to audio capture by using a memory pool to reduce allocations 58 | - Adds multi-threaded encoding 59 | 60 | ## 0.2.0 61 | ### MINOR Changes: 62 | - Fixes some timing issues that were causing audio / video desync. 63 | - Fixes `mp4` output containers. 64 | 65 | ## 0.1.0 66 | ### Initial Release: 67 | - Limited support 68 | - GPU accelerated framebuffer capture 69 | - nVidia GPUs only (1440p, 60fps, h264) 70 | - Pipewire audio only (stereo, 48Khz, aac) 71 | - Continuous capture to a specified file 72 | - Only reliably creates`.mkv` containers 73 | 74 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.27) 2 | 3 | file(READ ".versioning/current" current_version) 4 | string(STRIP "${current_version}" current_version) 5 | 6 | project( 7 | shadow-cast 8 | LANGUAGES CXX 9 | VERSION ${current_version} 10 | ) 11 | 12 | if (CMAKE_BUILD_TYPE STREQUAL "") 13 | message(STATUS "Defaulting CMAKE_BUILD_TYPE to Release") 14 | set(CMAKE_BUILD_TYPE "Release") 15 | endif() 16 | 17 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 18 | set(CMAKE_CXX_STANDARD 20) 19 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake) 20 | include(CPPCheck) 21 | include(GNUInstallDirs) 22 | include(CheckIPOSupported) 23 | check_ipo_supported( 24 | RESULT SHADOW_CAST_IPO_SUPPORTED 25 | OUTPUT SHADOW_CAST_IPO_SUPORTED_OUTPUT 26 | LANGUAGES CXX 27 | ) 28 | 29 | if (SHADOW_CAST_IPO_SUPPORTED) 30 | message(STATUS "IPO supported. Enabling") 31 | set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) 32 | else () 33 | message(STATUS "IPO not supported: ${SHADOW_CAST_IPO_SUPORTED_OUTPUT}") 34 | endif () 35 | 36 | string(TOLOWER ${CMAKE_CXX_BYTE_ORDER} SHADOW_CAST_BYTE_ORDER) 37 | 38 | add_compile_options(-Wall -Werror -Wextra -Wshadow) 39 | if (CMAKE_BUILD_TYPE STREQUAL "Debug") 40 | add_compile_options(-O0) 41 | endif() 42 | 43 | find_package(Pipewire REQUIRED) 44 | find_package( 45 | FFMpeg REQUIRED 46 | COMPONENTS 47 | avcodec 48 | avdevice 49 | avfilter 50 | avformat 51 | avutil 52 | swresample 53 | swscale 54 | ) 55 | find_package(X11 REQUIRED) 56 | find_package(DRM REQUIRED) 57 | 58 | option( 59 | SHADOW_CAST_ENABLE_TESTS 60 | "Enable tests for ${PROJECT_NAME}" 61 | OFF 62 | ) 63 | 64 | option( 65 | SHADOW_CAST_ENABLE_ASAN 66 | "Enable ASan for ${PROJECT_NAME}" 67 | OFF 68 | ) 69 | 70 | option( 71 | SHADOW_CAST_ENABLE_TSAN 72 | "Enable TSan for ${PROJECT_NAME}" 73 | OFF 74 | ) 75 | 76 | option( 77 | SHADOW_CAST_ENABLE_HISTOGRAMS 78 | "Enable audio and video histograms for ${PROJECT_NAME}" 79 | OFF 80 | ) 81 | 82 | if (SHADOW_CAST_ENABLE_ASAN) 83 | add_compile_options(-fsanitize=address) 84 | add_link_options(-fsanitize=address) 85 | endif() 86 | 87 | if (SHADOW_CAST_ENABLE_TSAN) 88 | add_compile_options(-fsanitize=thread) 89 | add_link_options(-fsanitize=thread) 90 | endif() 91 | 92 | add_subdirectory(src) 93 | 94 | if(SHADOW_CAST_ENABLE_TESTS) 95 | enable_testing() 96 | add_subdirectory(tests) 97 | endif() 98 | 99 | configure_file( 100 | tools/install-helper.sh.in 101 | install-helper.sh 102 | FILE_PERMISSIONS 103 | OWNER_READ OWNER_WRITE OWNER_EXECUTE 104 | GROUP_READ GROUP_WRITE 105 | WORLD_READ WORLD_WRITE 106 | @ONLY 107 | ) 108 | 109 | install( 110 | TARGETS shadow-cast shadow-cast-kms 111 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 112 | ) 113 | -------------------------------------------------------------------------------- /cmake/CPPCheck.cmake: -------------------------------------------------------------------------------- 1 | find_program(CMAKE_CXX_CPPCHECK NAMES cppcheck) 2 | if(CMAKE_CXX_CPPCHECK AND CMAKE_BUILD_TYPE STREQUAL "Debug") 3 | list(APPEND CMAKE_CXX_CPPCHECK 4 | "--check-level=exhaustive" 5 | "--inline-suppr" 6 | "--enable=warning" 7 | "--error-exitcode=1" 8 | ) 9 | else() 10 | set(CMAKE_CXX_CPPCHECK "") 11 | endif() 12 | -------------------------------------------------------------------------------- /cmake/EmbeddedGLSLTarget.cmake: -------------------------------------------------------------------------------- 1 | # This will create an object library target called `${NAME}.o` 2 | # which will embed and export the GLSL as a array of bytes. 3 | # The symbol name given to each export GLSL file is derived 4 | # from the entry in `SOURCES`... 5 | # 6 | # ``` 7 | # extern char const _private__start[]; 8 | # extern char const _private__end[]; 9 | # ``` 10 | # 11 | # The source file (combined with any path components) have all `.` 12 | # `-`, `/`, etc replaced with underscores (`_`) 13 | # 14 | # To keep the exported symbol names short, you should restrict 15 | # `SOURCES` to files in the current directory. Any relative 16 | # directories will also form part of the symbol name. E.g... 17 | # 18 | # ``` 19 | # add_embedded_glsl_target( 20 | # SOURCES glsl/vertex-shaders/shader.glsl 21 | # ... 22 | # ) 23 | # ``` 24 | # 25 | # would yield the following symbol names... 26 | # 27 | # ``` 28 | # extern char const _private_glsl_vertex_shaders_shader_glsl_start[]; 29 | # extern char const _private_glsl_vertex_shaders_shader_glsl_end[]; 30 | # ``` 31 | function(add_embedded_glsl_target) 32 | set(single_value_args NAME) 33 | set(multi_value_args SOURCES) 34 | cmake_parse_arguments( 35 | ADD_EMBEDDED_GLSL_TARGET 36 | "${options}" 37 | "${single_value_args}" 38 | "${multi_value_args}" 39 | ${ARGN} 40 | ) 41 | 42 | # We may be able to use the `-L` argument when generating an 43 | # object file, which could remove the path element from the 44 | # symbol names (`-L` is a search path)... 45 | add_custom_target( 46 | ${ADD_EMBEDDED_GLSL_TARGET_NAME}_generator 47 | COMMAND ${CMAKE_LINKER} -b binary -r 48 | -o "${CMAKE_CURRENT_BINARY_DIR}/${ADD_EMBEDDED_GLSL_TARGET_NAME}.o" 49 | ${ADD_EMBEDDED_GLSL_TARGET_SOURCES} 50 | WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" 51 | ) 52 | add_library(${ADD_EMBEDDED_GLSL_TARGET_NAME} OBJECT IMPORTED GLOBAL) 53 | set_target_properties( 54 | ${ADD_EMBEDDED_GLSL_TARGET_NAME} 55 | PROPERTIES 56 | IMPORTED_OBJECTS "${CMAKE_CURRENT_BINARY_DIR}/${ADD_EMBEDDED_GLSL_TARGET_NAME}.o" 57 | ) 58 | add_dependencies(${ADD_EMBEDDED_GLSL_TARGET_NAME} ${ADD_EMBEDDED_GLSL_TARGET_NAME}_generator) 59 | endfunction() 60 | -------------------------------------------------------------------------------- /cmake/FindDRM.cmake: -------------------------------------------------------------------------------- 1 | find_package(PkgConfig REQUIRED) 2 | include(FindPackageHandleStandardArgs) 3 | 4 | pkg_check_modules(PC_DRM QUIET "libdrm") 5 | 6 | find_path(DRM_INCLUDE_DIR 7 | NAMES drm.h 8 | PATHS "${PC_DRM_INCLUDE_DIRS}" 9 | PATH_SUFFIXES "libdrm" 10 | ) 11 | 12 | find_library(DRM_LIBRARY 13 | NAMES drm 14 | PATHS "${PC_DRM_LIBRARY_DIRS}" 15 | ) 16 | 17 | find_package_handle_standard_args(DRM 18 | FOUND_VAR DRM_FOUND 19 | REQUIRED_VARS 20 | DRM_LIBRARY 21 | DRM_INCLUDE_DIR 22 | ) 23 | 24 | if (DRM_FOUND) 25 | # Include the found directory. Needed for including 26 | # `xf86drm.h`... 27 | list(APPEND DRM_INCLUDE_DIRS "${DRM_INCLUDE_DIR}") 28 | 29 | # Set the include dir up one level so that dependent 30 | # targets have to use `#include `. This 31 | # prevents any clashing header names... 32 | if (DRM_INCLUDE_DIR MATCHES "\/libdrm$") 33 | get_filename_component( 34 | DRM_INCLUDE_DIR 35 | "${DRM_INCLUDE_DIR}" 36 | DIRECTORY 37 | ) 38 | list(APPEND DRM_INCLUDE_DIRS "${DRM_INCLUDE_DIR}") 39 | endif () 40 | set(DRM_LIBRARIES ${DRM_LIBRARY}) 41 | set(DRM_DEFINITIONS ${PC_DRM_CFLAGS_OTHER}) 42 | endif () 43 | 44 | if (DRM_FOUND) 45 | message(STATUS "DRM_INCLUDE_DIRS: ${DRM_INCLUDE_DIRS}") 46 | message(STATUS "DRM_LIBRARY: ${DRM_INCLUDE_DIR}") 47 | add_library(DRM::drm UNKNOWN IMPORTED) 48 | set_target_properties( 49 | DRM::drm 50 | PROPERTIES 51 | IMPORTED_LOCATION "${DRM_LIBRARY}" 52 | INTERFACE_COMPILE_OPTIONS "${PC_DRM_CGLAGS_OTHER}" 53 | INTERFACE_INCLUDE_DIRECTORIES "${DRM_INCLUDE_DIRS}" 54 | ) 55 | endif () 56 | -------------------------------------------------------------------------------- /cmake/FindFFMpeg.cmake: -------------------------------------------------------------------------------- 1 | find_package(PkgConfig REQUIRED) 2 | include(FindPackageHandleStandardArgs) 3 | 4 | function(try_find_ffmpeg_targets) 5 | cmake_parse_arguments( 6 | EXPORT_FFMPEG 7 | "" 8 | "" 9 | "NAMES" 10 | ${ARGN} 11 | ) 12 | 13 | foreach(FFMPEG_COMPONENT ${EXPORT_FFMPEG_NAMES}) 14 | pkg_check_modules(PC_${FFMPEG_COMPONENT} QUIET "lib${FFMPEG_COMPONENT}") 15 | 16 | find_path(FFMpeg_${FFMPEG_COMPONENT}_INCLUDE_DIR 17 | NAMES ${FFMPEG_COMPONENT}.h 18 | PATHS "${PC_${FFMPEG_COMPONENT}_INCLUDE_DIRS}" 19 | PATH_SUFFIXES "lib${FFMPEG_COMPONENT}" 20 | ) 21 | 22 | # libavutil contains a `time.h` file which will undoubtedly 23 | # clash horribly with standard library `time.h`. So we have 24 | # to remove the last path component and include these files 25 | # will then need to be referenced using their path suffix, 26 | # E.g... 27 | # 28 | # #include 29 | # 30 | if (FFMpeg_${FFMPEG_COMPONENT}_INCLUDE_DIR 31 | AND FFMpeg_${FFMPEG_COMPONENT}_INCLUDE_DIR MATCHES "\/lib${FFMPEG_COMPONENT}$") 32 | get_filename_component( 33 | FFMpeg_${FFMPEG_COMPONENT}_INCLUDE_DIR 34 | "${FFMpeg_${FFMPEG_COMPONENT}_INCLUDE_DIR}" 35 | DIRECTORY 36 | ) 37 | endif() 38 | 39 | find_library(FFMpeg_${FFMPEG_COMPONENT}_LIBRARY 40 | NAMES ${FFMPEG_COMPONENT} 41 | PATHS "${PC_${FFMPEG_COMPONENT}_LIBRARY_DIRS}" 42 | ) 43 | 44 | list(APPEND FFMPEG_COMPONENT_VARS "FFMpeg_${FFMPEG_COMPONENT}_INCLUDE_DIR") 45 | list(APPEND FFMPEG_COMPONENT_VARS "FFMpeg_${FFMPEG_COMPONENT}_LIBRARY") 46 | 47 | endforeach() 48 | 49 | message(STATUS "Components: ${FFMpeg_FIND_COMPONENTS}") 50 | 51 | foreach(COMP ${FFMpeg_FIND_COMPONENTS}) 52 | if (FFMpeg_${COMP}_LIBRARY AND FFMpeg_${COMP}_INCLUDE_DIR) 53 | message(STATUS "FFMpeg_${COMP}: FOUND") 54 | 55 | add_library(FFMpeg::${COMP} UNKNOWN IMPORTED) 56 | set_target_properties(FFMpeg::${COMP} 57 | PROPERTIES 58 | IMPORTED_LOCATION "${FFMpeg_${COMP}_LIBRARY}" 59 | INTERFACE_COMPILE_OPTIONS "${PC_${COMP}_CFLAGS_OTHER}" 60 | INTERFACE_INCLUDE_DIRECTORIES "${FFMpeg_${COMP}_INCLUDE_DIR}" 61 | ) 62 | set(FFMpeg_${COMP}_FOUND TRUE) 63 | else() 64 | set(FFMpeg_${COMP}_FOUND "NOTFOUND") 65 | endif() 66 | 67 | if(NOT FFMpeg_${COMP}_FOUND AND FFMpeg_FIND_REQUIRED_${COMP}) 68 | set(FFMpeg_FOUND "NOTFOUND" PARENT_SCOPE) 69 | break() 70 | else() 71 | set(FFMpeg_FOUND TRUE PARENT_SCOPE) 72 | endif() 73 | 74 | endforeach() 75 | 76 | endfunction(try_find_ffmpeg_targets) 77 | 78 | if (NOT FFMpeg_FIND_COMPONENTS) 79 | message(FATAL_ERROR "FindFFMpeg: No components were specified") 80 | endif() 81 | 82 | try_find_ffmpeg_targets( 83 | NAMES 84 | avcodec 85 | avdevice 86 | avfilter 87 | avformat 88 | avutil 89 | swresample 90 | swscale 91 | ) 92 | 93 | if (FFMpeg_FIND_REQUIRED AND NOT FFMpeg_FOUND) 94 | message(FATAL_ERROR "FFMpeg: Not all required components could be found") 95 | endif() 96 | 97 | set(FFMpeg_FOUND TRUE) 98 | -------------------------------------------------------------------------------- /cmake/FindPipewire.cmake: -------------------------------------------------------------------------------- 1 | find_package(PkgConfig REQUIRED) 2 | pkg_check_modules( 3 | PC_Pipewire 4 | QUIET 5 | pipewire 6 | ) 7 | 8 | find_path( 9 | Pipewire_INCLUDE_DIR 10 | NAMES pipewire.h 11 | PATHS ${PC_Pipewire_INCLUDE_DIRS} 12 | PATH_SUFFIXES pipewire-0.3/pipewire 13 | ) 14 | 15 | find_path( 16 | SPA_INCLUDE_DIR 17 | NAME plugin.h 18 | PATHS ${PC_Pipewire_INCLUDE_DIRS} 19 | PATH_SUFFIXES spa-0.2/spa/support 20 | ) 21 | 22 | find_library( 23 | Pipewire_LIBRARY 24 | NAMES pipewire-0.3 25 | PATHS ${PC_Pipewire_LIBRARY_DIRS} 26 | ) 27 | 28 | if (Pipewire_INCLUDE_DIR AND Pipewire_INCLUDE_DIR MATCHES "pipewire$") 29 | get_filename_component(Pipewire_INCLUDE_DIR "${Pipewire_INCLUDE_DIR}" DIRECTORY) 30 | endif() 31 | 32 | if (SPA_INCLUDE_DIR) 33 | get_filename_component(SPA_INCLUDE_DIR "${SPA_INCLUDE_DIR}" DIRECTORY) 34 | get_filename_component(SPA_INCLUDE_DIR "${SPA_INCLUDE_DIR}" DIRECTORY) 35 | endif() 36 | 37 | message(STATUS "Pipewire_INCLUDE_DIR: ${Pipewire_INCLUDE_DIR}") 38 | message(STATUS "Pipewire_LIBRARY: ${Pipewire_LIBRARY}") 39 | message(STATUS "SPA_INCLUDE_DIR: ${SPA_INCLUDE_DIR}") 40 | 41 | if (Pipewire_INCLUDE_DIR AND SPA_INCLUDE_DIR AND Pipewire_LIBRARY) 42 | add_library(Pipewire::pipewire UNKNOWN IMPORTED) 43 | set_target_properties( 44 | Pipewire::pipewire 45 | PROPERTIES 46 | IMPORTED_LOCATION "${Pipewire_LIBRARY}" 47 | INTERFACE_COMPILE_OPTIONS "${PC_Pipewire_CFLAGS_OTHER}" 48 | INTERFACE_INCLUDE_DIRECTORIES "${Pipewire_INCLUDE_DIR};${SPA_INCLUDE_DIR}" 49 | ) 50 | else() 51 | if (Pipewire_FIND_REQUIRED) 52 | message(FATAL_ERROR "Couldn't find required package: Pipewire") 53 | endif() 54 | endif() 55 | -------------------------------------------------------------------------------- /cmake/MakeTest.cmake: -------------------------------------------------------------------------------- 1 | function(make_test) 2 | set(single_value_args NAME) 3 | set(multi_value_args SOURCES ENV_VARS ENABLE_IF LABELS LINK_LIBRARIES) 4 | cmake_parse_arguments( 5 | MAKE_TEST 6 | "${options}" 7 | "${single_value_args}" 8 | "${multi_value_args}" 9 | ${ARGN} 10 | ) 11 | 12 | if(NOT MAKE_TEST_LABELS) 13 | set(MAKE_TEST_LABELS "default-tests") 14 | endif() 15 | 16 | add_executable(${MAKE_TEST_NAME} ${MAKE_TEST_SOURCES}) 17 | target_link_libraries(${MAKE_TEST_NAME} PRIVATE testing embedded_glsl shadow-cast-obj ${MAKE_TEST_LINK_LIBRARIES}) 18 | add_test(NAME ${MAKE_TEST_NAME} COMMAND ${MAKE_TEST_NAME}) 19 | set_tests_properties( 20 | ${MAKE_TEST_NAME} 21 | PROPERTIES 22 | ENVIRONMENT "${MAKE_TEST_ENV_VARS}" 23 | LABELS "${MAKE_TEST_LABELS}" 24 | ) 25 | 26 | # If the test doesn't specify any ENABLE_IF flags then 27 | # it is always included... 28 | list(LENGTH MAKE_TEST_ENABLE_IF enable_if_length) 29 | if(enable_if_length EQUAL 0) 30 | return() 31 | endif() 32 | 33 | # Loop through the ENABLE_IF flags. If any are found in 34 | # the test properties then we return early and the test 35 | # is included... 36 | while(enable_if_length GREATER 0) 37 | list(POP_FRONT MAKE_TEST_ENABLE_IF enable_if_val) 38 | if(${enable_if_val} IN_LIST SHADOW_CAST_ENABLE_TEST_CATEGORIES) 39 | return() 40 | endif() 41 | list(LENGTH MAKE_TEST_ENABLE_IF enable_if_length) 42 | endwhile() 43 | 44 | # At the point, there were ENABLE_IF flags specified but 45 | # non were found in the test properties, so disable it... 46 | set_tests_properties( 47 | ${MAKE_TEST_NAME} 48 | PROPERTIES 49 | DISABLED ON 50 | ) 51 | endfunction() 52 | -------------------------------------------------------------------------------- /doc/faq.md: -------------------------------------------------------------------------------- 1 | ### Q. Help, I'm getting the following error 2 | 3 | ``` 4 | ERROR: Couldn't create NvFBC instance 5 | ``` 6 | 7 | #### A. 8 | *Shadow Cast* uses the *NvFBC* facility to provide efficient, low-latency framebuffer capture on X11. By default, NVIDIA disables this on most (if not all) of its consumer-level GPUs. However, there are two ways around this restriction... 9 | 10 | - You can find a utility to patch your NVIDIA drivers in the [keylase/nvidia-patch](https://github.com/keylase/nvidia-patch) GitHub repo. 11 | - You can obtain a "key" to unlock this feature at runtime. The key can be set at runtime via the `SHADOW_CAST_NVFBC_KEY=` environment variable. I use this method but I'm not sure how "official" it is, so no keys are provided in this repo. Feel free to message me about this. 12 | 13 | ### Q. I'm capturing gameplay footage on X11 and my game is "laggy" 14 | 15 | #### A. 16 | In some games, the NVIDIA Capture library (*NvFBC*) appears to interact poorly with v-sync if your refresh rate matches the capture FPS. Try disabling v-sync. Another option you can try is to set the following environment variable when capturing... 17 | 18 | ``` 19 | $ SHADOW_CAST_STRICT_FPS=0 shadow-cast ... 20 | ``` 21 | 22 | Please note, however, using this option will scale the output video's frame rate to match NvFBC's closest match (e.g. `62.5` in the case of a 60fps capture). 23 | 24 | I haven't quite worked out the definitive cause of this "lagginess", but I suspect it is because NvFBC only accepts integer millisecond values as its sampling rate, so cannot exactly match *Shadow Cast*'s frame rate. For example, 60fps would require a fractional sampling frequency of `16.666` milliseconds, and NvFBC only allows either `16` or `17` milliseconds. 25 | -------------------------------------------------------------------------------- /doc/images/ghostrunner-wayland-detailed-audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmbeard/shadow-cast/6880aa157f97fc0b38581c6375fc9af35fbc7641/doc/images/ghostrunner-wayland-detailed-audio.png -------------------------------------------------------------------------------- /doc/images/ghostrunner-wayland-detailed-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmbeard/shadow-cast/6880aa157f97fc0b38581c6375fc9af35fbc7641/doc/images/ghostrunner-wayland-detailed-video.png -------------------------------------------------------------------------------- /doc/images/ghostrunner-wayland-smoothed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmbeard/shadow-cast/6880aa157f97fc0b38581c6375fc9af35fbc7641/doc/images/ghostrunner-wayland-smoothed.png -------------------------------------------------------------------------------- /doc/images/ghostrunner-x11-detailed-audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmbeard/shadow-cast/6880aa157f97fc0b38581c6375fc9af35fbc7641/doc/images/ghostrunner-x11-detailed-audio.png -------------------------------------------------------------------------------- /doc/images/ghostrunner-x11-detailed-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmbeard/shadow-cast/6880aa157f97fc0b38581c6375fc9af35fbc7641/doc/images/ghostrunner-x11-detailed-video.png -------------------------------------------------------------------------------- /doc/images/ghostrunner-x11-smoothed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmbeard/shadow-cast/6880aa157f97fc0b38581c6375fc9af35fbc7641/doc/images/ghostrunner-x11-smoothed.png -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | configure_file( 2 | config.hpp.in 3 | ${CMAKE_CURRENT_BINARY_DIR}/config.hpp 4 | ) 5 | 6 | add_library(shadow-cast-obj 7 | OBJECT 8 | av/buffer.cpp 9 | av/buffer_pool.cpp 10 | av/codec.cpp 11 | av/format.cpp 12 | av/frame.cpp 13 | av/media_chunk.cpp 14 | av/packet.cpp 15 | av/sample_format.cpp 16 | 17 | display/display.cpp 18 | 19 | drm/messaging.cpp 20 | drm/planes.cpp 21 | 22 | gl/buffer.cpp 23 | gl/core.cpp 24 | gl/error.cpp 25 | gl/framebuffer.cpp 26 | gl/program.cpp 27 | gl/shader.cpp 28 | gl/texture.cpp 29 | gl/vertex_array_object.cpp 30 | 31 | handlers/audio_chunk_writer.cpp 32 | handlers/drm_video_frame_writer.cpp 33 | handlers/stream_finalizer.cpp 34 | handlers/video_frame_writer.cpp 35 | 36 | io/accept_handler.cpp 37 | io/process.cpp 38 | io/signals.cpp 39 | io/unix_socket.cpp 40 | 41 | platform/egl.cpp 42 | platform/opengl.cpp 43 | platform/wayland.cpp 44 | 45 | metrics/metrics.cpp 46 | 47 | services/audio_service.cpp 48 | services/color_converter.cpp 49 | services/context.cpp 50 | services/drm_video_service.cpp 51 | services/encoder.cpp 52 | services/encoder_service.cpp 53 | services/readiness.cpp 54 | services/service.cpp 55 | services/service_registry.cpp 56 | services/signal_service.cpp 57 | services/video_service.cpp 58 | 59 | utils/base64.cpp 60 | utils/cmd_line.cpp 61 | utils/contracts.cpp 62 | utils/elapsed.cpp 63 | utils/frame_time.cpp 64 | utils/result.cpp 65 | 66 | error.cpp 67 | logging.cpp 68 | nvidia.cpp 69 | ) 70 | 71 | target_include_directories(shadow-cast-obj 72 | PUBLIC 73 | ${CMAKE_CURRENT_LIST_DIR} 74 | ${CMAKE_CURRENT_BINARY_DIR} 75 | ) 76 | 77 | target_link_libraries(shadow-cast-obj 78 | PUBLIC 79 | Pipewire::pipewire 80 | FFMpeg::avcodec 81 | FFMpeg::avdevice 82 | FFMpeg::avfilter 83 | FFMpeg::avformat 84 | FFMpeg::avutil 85 | FFMpeg::swresample 86 | FFMpeg::swscale 87 | pthread 88 | X11::X11 89 | wayland-client 90 | wayland-egl 91 | ) 92 | 93 | add_subdirectory(glsl) 94 | 95 | add_executable(shadow-cast 96 | main.cpp 97 | ) 98 | 99 | target_link_libraries(shadow-cast 100 | PRIVATE 101 | embedded_glsl 102 | shadow-cast-obj 103 | ) 104 | 105 | set( 106 | SHADOW_CAST_KMS_BINARY_NAME 107 | "shadow-cast-kms" 108 | PARENT_SCOPE 109 | ) 110 | 111 | add_executable(shadow-cast-kms 112 | kms.cpp 113 | ) 114 | 115 | set_target_properties( 116 | shadow-cast-kms 117 | PROPERTIES 118 | OUTPUT_NAME "${SHADOW_CAST_KMS_BINARY_NAME}" 119 | ) 120 | 121 | target_link_libraries(shadow-cast-kms 122 | PRIVATE 123 | embedded_glsl 124 | shadow-cast-obj 125 | DRM::drm 126 | ) 127 | -------------------------------------------------------------------------------- /src/av.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_AV_HPP_INCLUDED 2 | #define SHADOW_CAST_AV_HPP_INCLUDED 3 | 4 | #include "./av/buffer.hpp" 5 | #include "./av/buffer_pool.hpp" 6 | #include "./av/codec.hpp" 7 | #include "./av/format.hpp" 8 | #include "./av/frame.hpp" 9 | #include "./av/fwd.hpp" 10 | #include "./av/media_chunk.hpp" 11 | #include "./av/packet.hpp" 12 | #include "./av/sample_format.hpp" 13 | 14 | #endif // SHADOW_CAST_AV_HPP_INCLUDED 15 | -------------------------------------------------------------------------------- /src/av/buffer.cpp: -------------------------------------------------------------------------------- 1 | #include "av/buffer.hpp" 2 | 3 | namespace sc 4 | { 5 | 6 | auto BufferDeleter::operator()(AVBufferRef* ptr) const noexcept -> void 7 | { 8 | av_buffer_unref(&ptr); 9 | } 10 | 11 | auto initialize_writable_buffer(FramePtr::pointer frame) -> void 12 | { 13 | char error_buffer[AV_ERROR_MAX_STRING_SIZE]; 14 | if (auto averr = av_frame_get_buffer(frame, 0); averr < 0) { 15 | av_make_error_string(error_buffer, AV_ERROR_MAX_STRING_SIZE, averr); 16 | throw std::runtime_error { error_buffer }; 17 | } 18 | if (av_frame_make_writable(frame) < 0) 19 | throw std::runtime_error { "Couldn't make frame writable" }; 20 | } 21 | } // namespace sc 22 | -------------------------------------------------------------------------------- /src/av/buffer.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_AV_BUFFER_HPP_INCLUDED 2 | #define SHADOW_CAST_AV_BUFFER_HPP_INCLUDED 3 | 4 | #include "av/frame.hpp" 5 | #include "av/fwd.hpp" 6 | #include 7 | namespace sc 8 | { 9 | 10 | struct BufferDeleter 11 | { 12 | auto operator()(AVBufferRef* ptr) const noexcept -> void; 13 | }; 14 | 15 | using BufferPtr = std::unique_ptr; 16 | 17 | auto initialize_writable_buffer(FramePtr::pointer) -> void; 18 | 19 | } // namespace sc 20 | 21 | #endif // SHADOW_CAST_AV_BUFFER_HPP_INCLUDED 22 | -------------------------------------------------------------------------------- /src/av/buffer_pool.cpp: -------------------------------------------------------------------------------- 1 | #include "av/buffer_pool.hpp" 2 | 3 | namespace sc 4 | { 5 | 6 | auto BufferPoolDeleter::operator()(AVBufferPool* ptr) const noexcept -> void 7 | { 8 | av_buffer_pool_uninit(&ptr); 9 | } 10 | 11 | } // namespace sc 12 | -------------------------------------------------------------------------------- /src/av/buffer_pool.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_AV_BUFFER_POOL_HPP_INCLUDED 2 | #define SHADOW_CAST_AV_BUFFER_POOL_HPP_INCLUDED 3 | 4 | #include "av/fwd.hpp" 5 | #include 6 | 7 | namespace sc 8 | { 9 | struct BufferPoolDeleter 10 | { 11 | auto operator()(AVBufferPool* ptr) const noexcept -> void; 12 | }; 13 | 14 | using BufferPoolPtr = std::unique_ptr; 15 | } // namespace sc 16 | #endif // SHADOW_CAST_AV_BUFFER_POOL_HPP_INCLUDED 17 | -------------------------------------------------------------------------------- /src/av/codec.cpp: -------------------------------------------------------------------------------- 1 | #include "av/codec.hpp" 2 | #include "av/buffer.hpp" 3 | #include "error.hpp" 4 | #include "nvidia.hpp" 5 | #include 6 | #include 7 | extern "C" { 8 | #include 9 | } 10 | 11 | namespace sc 12 | { 13 | auto CodecContextDeleter::operator()(AVCodecContext* ptr) noexcept -> void 14 | { 15 | avcodec_free_context(&ptr); 16 | } 17 | 18 | auto create_video_encoder(std::string const& encoder_name, 19 | CUcontext cuda_ctx, 20 | AVBufferPool* pool, 21 | VideoOutputSize size, 22 | FrameTime const& ft, 23 | AVPixelFormat pixel_format) -> sc::CodecContextPtr 24 | { 25 | sc::BorrowedPtr video_encoder { avcodec_find_encoder_by_name( 26 | encoder_name.c_str()) }; 27 | if (!video_encoder) { 28 | throw CodecError { "Failed to find required video codec" }; 29 | } 30 | 31 | sc::CodecContextPtr video_encoder_context { avcodec_alloc_context3( 32 | video_encoder.get()) }; 33 | video_encoder_context->codec_id = video_encoder->id; 34 | auto const timebase = ft.fps_ratio(); 35 | video_encoder_context->time_base = timebase; 36 | video_encoder_context->framerate.num = timebase.den; 37 | video_encoder_context->framerate.den = timebase.num; 38 | video_encoder_context->sample_aspect_ratio = AVRational { 1, 1 }; 39 | video_encoder_context->max_b_frames = 0; 40 | video_encoder_context->pix_fmt = AV_PIX_FMT_CUDA; 41 | video_encoder_context->bit_rate = 100'000; 42 | video_encoder_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; 43 | video_encoder_context->width = size.width; 44 | video_encoder_context->height = size.height; 45 | 46 | sc::BufferPtr device_ctx { av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA) }; 47 | if (!device_ctx) { 48 | throw std::runtime_error { "Failed to allocate H/W device context" }; 49 | } 50 | 51 | AVHWDeviceContext* hw_device_context = 52 | reinterpret_cast(device_ctx->data); 53 | AVCUDADeviceContext* cuda_device_context = 54 | reinterpret_cast(hw_device_context->hwctx); 55 | cuda_device_context->cuda_ctx = cuda_ctx; 56 | if (auto const ret = av_hwdevice_ctx_init(device_ctx.get()); ret < 0) { 57 | throw std::runtime_error { "Failed to initialize H/W device context: " + 58 | av_error_to_string(ret) }; 59 | } 60 | 61 | sc::BufferPtr frame_context { av_hwframe_ctx_alloc(device_ctx.get()) }; 62 | if (!frame_context) { 63 | throw std::runtime_error { "Failed to allocate H/W frame context" }; 64 | } 65 | 66 | AVHWFramesContext* hw_frame_context = 67 | reinterpret_cast(frame_context->data); 68 | hw_frame_context->width = video_encoder_context->width; 69 | hw_frame_context->height = video_encoder_context->height; 70 | hw_frame_context->sw_format = pixel_format; 71 | hw_frame_context->format = video_encoder_context->pix_fmt; 72 | 73 | hw_frame_context->pool = pool; 74 | hw_frame_context->initial_pool_size = 1; 75 | 76 | if (auto const ret = av_hwframe_ctx_init(frame_context.get()); ret < 0) { 77 | throw std::runtime_error { "Failed to initialize H/W frame context: " + 78 | av_error_to_string(ret) }; 79 | } 80 | 81 | video_encoder_context->hw_frames_ctx = av_buffer_ref(frame_context.get()); 82 | 83 | AVDictionary* options = nullptr; 84 | av_dict_set_int(&options, "qp", 21, 0); 85 | av_dict_set(&options, "preset", "p5", 0); 86 | 87 | if (auto const ret = avcodec_open2( 88 | video_encoder_context.get(), video_encoder.get(), &options); 89 | ret < 0) { 90 | throw CodecError { "Failed to open video codec: " + 91 | av_error_to_string(ret) }; 92 | } 93 | 94 | return video_encoder_context; 95 | } 96 | 97 | } // namespace sc 98 | -------------------------------------------------------------------------------- /src/av/codec.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_AV_CODEC_HPP_INCLUDED 2 | #define SHADOW_CAST_AV_CODEC_HPP_INCLUDED 3 | 4 | #include "av/fwd.hpp" 5 | #include "display/display.hpp" 6 | #include "nvidia.hpp" 7 | #include 8 | #include 9 | 10 | namespace sc 11 | { 12 | struct CodecContextDeleter 13 | { 14 | auto operator()(AVCodecContext* ptr) noexcept -> void; 15 | }; 16 | 17 | struct VideoOutputSize 18 | { 19 | std::uint32_t width; 20 | std::uint32_t height; 21 | }; 22 | 23 | using CodecContextPtr = std::unique_ptr; 24 | auto create_video_encoder(std::string const& encoder_name, 25 | CUcontext cuda_ctx, 26 | AVBufferPool* pool, 27 | VideoOutputSize size, 28 | FrameTime const& ft, 29 | AVPixelFormat pixel_format) -> sc::CodecContextPtr; 30 | } // namespace sc 31 | 32 | #endif // SHADOW_CAST_AV_CODEC_HPP_INCLUDED 33 | -------------------------------------------------------------------------------- /src/av/format.cpp: -------------------------------------------------------------------------------- 1 | #include "av/format.hpp" 2 | 3 | namespace sc 4 | { 5 | 6 | auto FormatContextDeleter::operator()(AVFormatContext* ptr) noexcept -> void 7 | { 8 | avformat_free_context(ptr); 9 | } 10 | 11 | } // namespace sc 12 | -------------------------------------------------------------------------------- /src/av/format.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_AV_FORMAT_HPP_INCLUDED 2 | #define SHADOW_CAST_AV_FORMAT_HPP_INCLUDED 3 | 4 | #include 5 | 6 | extern "C" { 7 | #include 8 | } 9 | 10 | namespace sc 11 | { 12 | 13 | struct FormatContextDeleter 14 | { 15 | auto operator()(AVFormatContext* ptr) noexcept -> void; 16 | }; 17 | 18 | using FormatContextPtr = std::unique_ptr; 19 | 20 | } // namespace sc 21 | 22 | #endif // SHADOW_CAST_AV_FORMAT_HPP_INCLUDED 23 | -------------------------------------------------------------------------------- /src/av/frame.cpp: -------------------------------------------------------------------------------- 1 | #include "av/frame.hpp" 2 | #include "av/packet.hpp" 3 | #include "error.hpp" 4 | #include 5 | #include 6 | 7 | std::mutex send_frame_mutex {}; 8 | 9 | namespace 10 | { 11 | template 12 | auto invoke_synchronized(std::mutex& mutex, F&& f) 13 | { 14 | std::lock_guard lock { mutex }; 15 | return std::forward(f)(); 16 | } 17 | 18 | } // namespace 19 | 20 | namespace sc 21 | { 22 | 23 | auto FrameDeleter::operator()(AVFrame* ptr) const noexcept -> void 24 | { 25 | av_frame_free(&ptr); 26 | } 27 | 28 | AVFrameUnrefGuard::~AVFrameUnrefGuard() 29 | { 30 | if (frame) 31 | av_frame_unref(frame.get()); 32 | } 33 | 34 | auto send_frame(AVFrame* frame, 35 | AVCodecContext* ctx, 36 | AVFormatContext* fmt, 37 | AVStream* stream, 38 | AVPacket* packet) -> void 39 | { 40 | auto response = avcodec_send_frame(ctx, frame); 41 | 42 | while (response >= 0) { 43 | response = avcodec_receive_packet(ctx, packet); 44 | if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) { 45 | break; 46 | } 47 | 48 | if (response < 0) { 49 | throw std::runtime_error { "receive packet error" }; 50 | } 51 | 52 | PacketUnrefGuard packet_unref_guard { packet }; 53 | 54 | packet->stream_index = stream->index; 55 | av_packet_rescale_ts(packet, ctx->time_base, stream->time_base); 56 | 57 | response = invoke_synchronized(send_frame_mutex, [&] { 58 | return av_interleaved_write_frame(fmt, packet); 59 | }); 60 | if (response < 0) { 61 | throw std::runtime_error { "write packet error: " + 62 | av_error_to_string(response) }; 63 | } 64 | } 65 | } 66 | } // namespace sc 67 | -------------------------------------------------------------------------------- /src/av/frame.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_AV_FRAME_HPP_INCLUDED 2 | #define SHADOW_CAST_AV_FRAME_HPP_INCLUDED 3 | 4 | #include "av/fwd.hpp" 5 | #include "utils/borrowed_ptr.hpp" 6 | #include 7 | 8 | namespace sc 9 | { 10 | struct FrameDeleter 11 | { 12 | auto operator()(AVFrame* ptr) const noexcept -> void; 13 | }; 14 | 15 | using FramePtr = std::unique_ptr; 16 | 17 | struct AVFrameUnrefGuard 18 | { 19 | ~AVFrameUnrefGuard(); 20 | sc::BorrowedPtr frame; 21 | }; 22 | 23 | auto send_frame(AVFrame* frame, 24 | AVCodecContext* ctx, 25 | AVFormatContext* fmt, 26 | AVStream* stream, 27 | AVPacket* packet) -> void; 28 | 29 | } // namespace sc 30 | 31 | #endif // SHADOW_CAST_AV_FRAME_HPP_INCLUDED 32 | -------------------------------------------------------------------------------- /src/av/fwd.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_AV_FWD_HPP_INCLUDED 2 | #define SHADOW_CAST_AV_FWD_HPP_INCLUDED 3 | 4 | extern "C" { 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | // #include 12 | #include 13 | #include 14 | } 15 | 16 | #endif // SHADOW_CAST_AV_FWD_HPP_INCLUDED 17 | -------------------------------------------------------------------------------- /src/av/media_chunk.cpp: -------------------------------------------------------------------------------- 1 | #include "av/media_chunk.hpp" 2 | #include 3 | 4 | namespace sc 5 | { 6 | auto DynamicBuffer::reset() noexcept -> void { bytes_committed_ = 0; } 7 | 8 | auto DynamicBuffer::prepare(std::size_t n) -> std::span 9 | { 10 | if (n > capacity()) 11 | data_.resize(data_.size() + (n - capacity())); 12 | 13 | return std::span { next(begin(data_), bytes_committed_), end(data_) }; 14 | } 15 | 16 | auto DynamicBuffer::commit(std::size_t n) -> void 17 | { 18 | assert(n <= (data_.size() - bytes_committed_)); 19 | bytes_committed_ += n; 20 | } 21 | 22 | auto DynamicBuffer::consume(std::size_t n) -> void 23 | { 24 | assert(n <= bytes_committed_); 25 | data_.erase(begin(data_), next(begin(data_), n)); 26 | 27 | bytes_committed_ -= n; 28 | } 29 | 30 | auto DynamicBuffer::data() noexcept -> std::span 31 | { 32 | return std::span { data_.data(), bytes_committed_ }; 33 | } 34 | 35 | auto DynamicBuffer::data() const noexcept -> std::span 36 | { 37 | return std::span { data_.data(), bytes_committed_ }; 38 | } 39 | 40 | auto DynamicBuffer::size() const noexcept -> std::size_t 41 | { 42 | return bytes_committed_; 43 | } 44 | 45 | auto DynamicBuffer::capacity() const noexcept -> std::size_t 46 | { 47 | return data_.size() - size(); 48 | } 49 | 50 | auto MediaChunk::reset() noexcept -> void 51 | { 52 | timestamp_ms = 0; 53 | sample_count = 0; 54 | for (auto& b : channel_buffers()) 55 | b.reset(); 56 | } 57 | 58 | auto MediaChunk::channel_buffers() noexcept -> std::vector& 59 | { 60 | return buffers_; 61 | } 62 | 63 | auto MediaChunk::channel_buffers() const noexcept 64 | -> std::vector const& 65 | { 66 | return buffers_; 67 | } 68 | 69 | } // namespace sc 70 | -------------------------------------------------------------------------------- /src/av/media_chunk.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_AV_MEDIA_CHUNK_HPP_INCLUDED 2 | #define SHADOW_CAST_AV_MEDIA_CHUNK_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "utils/intrusive_list.hpp" 10 | 11 | namespace sc 12 | { 13 | struct DynamicBuffer 14 | { 15 | auto prepare(std::size_t) -> std::span; 16 | auto commit(std::size_t) -> void; 17 | auto consume(std::size_t) -> void; 18 | auto data() noexcept -> std::span; 19 | auto data() const noexcept -> std::span; 20 | auto size() const noexcept -> std::size_t; 21 | auto capacity() const noexcept -> std::size_t; 22 | auto reset() noexcept -> void; 23 | 24 | private: 25 | std::vector data_; 26 | std::size_t bytes_committed_ { 0 }; 27 | }; 28 | 29 | struct MediaChunk : ListItemBase 30 | { 31 | std::size_t timestamp_ms { 0 }; 32 | std::size_t sample_count { 0 }; 33 | auto channel_buffers() noexcept -> std::vector&; 34 | auto channel_buffers() const noexcept -> std::vector const&; 35 | auto reset() noexcept -> void; 36 | 37 | private: 38 | std::vector buffers_; 39 | }; 40 | } // namespace sc 41 | 42 | #endif // SHADOW_CAST_AV_MEDIA_CHUNK_HPP_INCLUDED 43 | -------------------------------------------------------------------------------- /src/av/packet.cpp: -------------------------------------------------------------------------------- 1 | #include "av/packet.hpp" 2 | 3 | namespace sc 4 | { 5 | auto PacketPtrDeleter::operator()(AVPacket* ptr) const noexcept -> void 6 | { 7 | av_packet_free(&ptr); 8 | } 9 | 10 | PacketUnrefGuard::~PacketUnrefGuard() { av_packet_unref(packet); } 11 | 12 | } // namespace sc 13 | -------------------------------------------------------------------------------- /src/av/packet.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_AV_PACKET_HPP_INCLUDED 2 | #define SHADOW_CAST_AV_PACKET_HPP_INCLUDED 3 | 4 | #include "./fwd.hpp" 5 | #include 6 | 7 | namespace sc 8 | { 9 | 10 | struct PacketPtrDeleter 11 | { 12 | auto operator()(AVPacket* ptr) const noexcept -> void; 13 | }; 14 | 15 | struct PacketUnrefGuard 16 | { 17 | ~PacketUnrefGuard(); 18 | 19 | AVPacket* packet; 20 | }; 21 | 22 | using PacketPtr = std::unique_ptr; 23 | 24 | } // namespace sc 25 | #endif // SHADOW_CAST_AV_PACKET_HPP_INCLUDED 26 | -------------------------------------------------------------------------------- /src/av/sample_format.cpp: -------------------------------------------------------------------------------- 1 | #include "av/sample_format.hpp" 2 | #include 3 | 4 | namespace sc 5 | { 6 | auto find_supported_formats(sc::BorrowedPtr codec) 7 | -> std::vector 8 | { 9 | std::vector results; 10 | for (auto supported_fmt = codec->sample_fmts; 11 | supported_fmt && *supported_fmt >= 0; 12 | ++supported_fmt) { 13 | results.push_back(convert_from_libav_format(*supported_fmt)); 14 | } 15 | 16 | std::sort(rbegin(results), rend(results)); 17 | 18 | return results; 19 | } 20 | 21 | auto find_supported_sample_rates(sc::BorrowedPtr codec) noexcept 22 | -> std::span 23 | { 24 | auto const* p = codec->supported_samplerates; 25 | 26 | if (!p) 27 | return {}; 28 | 29 | auto const* end = p; 30 | while (end && *end) 31 | end++; 32 | 33 | return { p, static_cast(end - p) }; 34 | } 35 | 36 | auto is_sample_rate_supported(std::uint32_t requested, 37 | sc::BorrowedPtr codec) noexcept 38 | -> bool 39 | { 40 | auto supported = find_supported_sample_rates(codec); 41 | 42 | auto const pos = std::find(supported.begin(), supported.end(), requested); 43 | if (!supported.size() || pos != supported.end()) 44 | return true; 45 | 46 | return false; 47 | } 48 | 49 | auto sample_format_name(SampleFormat fmt) noexcept -> char const* 50 | { 51 | switch (fmt) { 52 | case SampleFormat::u8_interleaved: 53 | return "u8_interleaved"; 54 | case SampleFormat::s16_interleaved: 55 | return "s16_interleaved"; 56 | case SampleFormat::s32_interleaved: 57 | return "s32_interleaved"; 58 | case SampleFormat::float_interleaved: 59 | return "float_interleaved"; 60 | case SampleFormat::double_interleaved: 61 | return "double_interleaved"; 62 | case SampleFormat::u8_planar: 63 | return "u8_planar"; 64 | case SampleFormat::s16_planar: 65 | return "s16_planar"; 66 | case SampleFormat::s32_planar: 67 | return "s32_planar"; 68 | case SampleFormat::float_planar: 69 | return "float_planar"; 70 | case SampleFormat::double_planar: 71 | return "double_planar"; 72 | case SampleFormat::s64_interleaved: 73 | return "s64_interleaved"; 74 | case SampleFormat::s64_planar: 75 | return "s64_planar"; 76 | default: 77 | return "unknown"; 78 | } 79 | } 80 | 81 | } // namespace sc 82 | -------------------------------------------------------------------------------- /src/config.hpp.in: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_CONFIG_HPP_INCLUDED 2 | #define SHADOW_CAST_CONFIG_HPP_INCLUDED 3 | 4 | #cmakedefine SHADOW_CAST_ENABLE_HISTOGRAMS 5 | 6 | namespace sc 7 | { 8 | enum struct ByteOrder 9 | { 10 | little_endian, 11 | big_endian 12 | }; 13 | 14 | // clang-format off 15 | [[maybe_unused]] auto constexpr kByteOrder = ByteOrder::@SHADOW_CAST_BYTE_ORDER@; 16 | [[maybe_unused]] auto constexpr kShadowCastProg = "@PROJECT_NAME@"; 17 | [[maybe_unused]] auto constexpr kShadowCastVersion = "@PROJECT_VERSION@"; 18 | // clang-format on 19 | } // namespace sc 20 | 21 | #endif // SHADOW_CAST_CONFIG_HPP_INCLUDED 22 | -------------------------------------------------------------------------------- /src/display.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_DISPLAY_HPP_INCLUDED 2 | #define SHADOW_CAST_DISPLAY_HPP_INCLUDED 3 | 4 | #include "./display/display.hpp" 5 | 6 | #endif // SHADOW_CAST_DISPLAY_HPP_INCLUDED 7 | -------------------------------------------------------------------------------- /src/display/display.cpp: -------------------------------------------------------------------------------- 1 | #include "display/display.hpp" 2 | 3 | namespace sc 4 | { 5 | 6 | auto XDisplayDeleter::operator()(Display* ptr) const noexcept -> void 7 | { 8 | XCloseDisplay(ptr); 9 | } 10 | 11 | auto get_display() -> XDisplayPtr 12 | { 13 | XDisplayPtr display { XOpenDisplay(nullptr) }; 14 | if (!display) 15 | throw std::runtime_error { "Failed to open X display" }; 16 | 17 | return display; 18 | } 19 | 20 | } // namespace sc 21 | -------------------------------------------------------------------------------- /src/display/display.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_DISPLAY_DISPLAY_HPP_INCLUDED 2 | #define SHADOW_CAST_DISPLAY_DISPLAY_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | 7 | namespace sc 8 | { 9 | struct XDisplayDeleter 10 | { 11 | auto operator()(Display* ptr) const noexcept -> void; 12 | }; 13 | 14 | using XDisplayPtr = std::unique_ptr; 15 | 16 | auto get_display() -> XDisplayPtr; 17 | } // namespace sc 18 | 19 | #endif // SHADOW_CAST_DISPLAY_DISPLAY_HPP_INCLUDED 20 | -------------------------------------------------------------------------------- /src/drm.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_DRM_HPP_INCLUDED 2 | #define SHADOW_CAST_DRM_HPP_INCLUDED 3 | 4 | #include "./drm/messaging.hpp" 5 | #include "./drm/planes.hpp" 6 | 7 | #endif // SHADOW_CAST_DRM_HPP_INCLUDED 8 | -------------------------------------------------------------------------------- /src/drm/messaging.cpp: -------------------------------------------------------------------------------- 1 | #include "drm/messaging.hpp" 2 | #include 3 | #include 4 | 5 | namespace sc 6 | { 7 | 8 | auto DRMResponseSendHandler::operator()(int fd, 9 | msghdr& msg, 10 | DRMResponse const& response) noexcept 11 | -> ssize_t 12 | { 13 | /* The use of the control messages is significant 14 | * here; They are the _only_ valid way to transport 15 | * file descriptors via a unix socket. The kernel 16 | * must somehow know to marshal them correctly 17 | * between processes. 18 | * NOTE: _It's important to set the length of 19 | * the control message (`cmsg_len`) to the exact 20 | * size of the number of descriptors. Setting this 21 | * value higher cause issues when subsequently using 22 | * the descriptors_ 23 | */ 24 | 25 | SC_EXPECT(response.num_fds <= kMaxPlaneDescriptors); 26 | char cmsgbuf[CMSG_SPACE(sizeof(int) * kMaxPlaneDescriptors)]; 27 | std::memset(cmsgbuf, 0, sizeof(int) * kMaxPlaneDescriptors); 28 | 29 | if (response.num_fds) { 30 | 31 | msg.msg_control = cmsgbuf; 32 | msg.msg_controllen = CMSG_SPACE(sizeof(int) * response.num_fds); 33 | 34 | cmsghdr* cmsg = CMSG_FIRSTHDR(&msg); 35 | cmsg->cmsg_level = SOL_SOCKET; 36 | cmsg->cmsg_type = SCM_RIGHTS; 37 | cmsg->cmsg_len = CMSG_LEN(sizeof(int) * response.num_fds); 38 | 39 | int* fds = reinterpret_cast(CMSG_DATA(cmsg)); 40 | std::span descriptors { response.descriptors, 41 | response.num_fds }; 42 | for (auto const& desc : descriptors) 43 | *fds++ = desc.fd; 44 | } 45 | 46 | return ::sendmsg(fd, &msg, 0); 47 | } 48 | 49 | auto DRMResponseReceiveHandler::operator()(int fd, 50 | msghdr& msg, 51 | DRMResponse& response) noexcept 52 | -> ssize_t 53 | { 54 | /* Here, we pluck the file descriptors from the control 55 | * message and update the response message payload 56 | */ 57 | 58 | char cmsgbuf[CMSG_SPACE(sizeof(int) * kMaxPlaneDescriptors)] {}; 59 | msg.msg_control = cmsgbuf; 60 | msg.msg_controllen = sizeof(cmsgbuf); 61 | 62 | int res = ::recvmsg(fd, &msg, MSG_WAITALL); 63 | if (res <= 0) 64 | return res; 65 | 66 | if (response.num_fds > 0) { 67 | cmsghdr* cmsg = CMSG_FIRSTHDR(&msg); 68 | std::span fds { reinterpret_cast(CMSG_DATA(cmsg)), 69 | response.num_fds }; 70 | 71 | SC_EXPECT(response.num_fds <= fds.size()); 72 | for (std::uint32_t i = 0; i < response.num_fds; ++i) { 73 | response.descriptors[i].fd = fds[i]; 74 | } 75 | } 76 | 77 | return res; 78 | } 79 | 80 | } // namespace sc 81 | -------------------------------------------------------------------------------- /src/drm/messaging.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_DRM_MESSAGING_HPP_INCLUDED 2 | #define SHADOW_CAST_DRM_MESSAGING_HPP_INCLUDED 3 | 4 | #include "drm/planes.hpp" 5 | #include "io/message_handler.hpp" 6 | 7 | namespace sc 8 | { 9 | 10 | std::size_t constexpr kMaxPlaneDescriptors = 8; 11 | 12 | namespace drm_request 13 | { 14 | [[maybe_unused]] std::uint32_t constexpr kGetPlanes = 1; 15 | [[maybe_unused]] std::uint32_t constexpr kStop = 2; 16 | } // namespace drm_request 17 | 18 | struct DRMRequest 19 | { 20 | std::uint32_t type; 21 | }; 22 | 23 | struct DRMResponse 24 | { 25 | std::uint32_t result; 26 | std::uint32_t num_fds; 27 | PlaneDescriptor descriptors[kMaxPlaneDescriptors]; 28 | }; 29 | 30 | struct DRMResponseSendHandler 31 | { 32 | auto operator()(int, msghdr&, DRMResponse const&) noexcept -> ssize_t; 33 | }; 34 | 35 | struct DRMResponseReceiveHandler 36 | { 37 | auto operator()(int, msghdr&, DRMResponse&) noexcept -> ssize_t; 38 | }; 39 | 40 | using DRMResponseSender = 41 | MessageHandler; 42 | 43 | using DRMResponseReceiver = 44 | MessageHandler; 45 | 46 | } // namespace sc 47 | 48 | #endif // SHADOW_CAST_DRM_MESSAGING_HPP_INCLUDED 49 | -------------------------------------------------------------------------------- /src/drm/planes.cpp: -------------------------------------------------------------------------------- 1 | #include "drm/planes.hpp" 2 | 3 | namespace sc 4 | { 5 | 6 | auto PlaneDescriptor::set_flag(plane_flags::PlaneFlags flag) noexcept -> void 7 | { 8 | flags |= flag; 9 | } 10 | 11 | auto PlaneDescriptor::is_flag_set(plane_flags::PlaneFlags flag) const noexcept 12 | -> bool 13 | { 14 | return (flags & flag) != 0; 15 | } 16 | 17 | } // namespace sc 18 | -------------------------------------------------------------------------------- /src/drm/planes.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_DRM_PLANES_HPP_INCLUDED 2 | #define SHADOW_CAST_DRM_PLANES_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | 7 | namespace sc 8 | { 9 | 10 | namespace plane_flags 11 | { 12 | 13 | enum PlaneFlags : std::uint32_t 14 | { 15 | IS_CURSOR = (1 << 0), 16 | IS_COMBINED = (1 << 1) 17 | }; 18 | 19 | } 20 | 21 | struct PlaneDescriptor 22 | { 23 | int fd; 24 | uint32_t width; 25 | uint32_t height; 26 | uint32_t pitch; 27 | uint32_t offset; 28 | uint32_t pixel_format; 29 | uint64_t modifier; 30 | uint32_t connector_id; 31 | uint32_t flags; 32 | // bool is_combined_plane; 33 | // bool is_cursor; 34 | int x; 35 | int y; 36 | int src_w; 37 | int src_h; 38 | 39 | auto set_flag(plane_flags::PlaneFlags /* flag */) noexcept -> void; 40 | auto is_flag_set(plane_flags::PlaneFlags /* flag */) const noexcept -> bool; 41 | }; 42 | } // namespace sc 43 | 44 | #endif // SHADOW_CAST_DRM_PLANES_HPP_INCLUDED 45 | -------------------------------------------------------------------------------- /src/error.cpp: -------------------------------------------------------------------------------- 1 | #include "./error.hpp" 2 | 3 | namespace sc 4 | { 5 | ModuleError::ModuleError(std::string const& msg) 6 | : std::runtime_error { msg } 7 | { 8 | } 9 | 10 | CodecError::CodecError(std::string const& msg) 11 | : std::runtime_error { msg } 12 | { 13 | } 14 | 15 | FormatError::FormatError(std::string const& msg) 16 | : std::runtime_error { msg } 17 | { 18 | } 19 | 20 | IOError::IOError(std::string const& msg) 21 | : std::runtime_error { msg } 22 | { 23 | } 24 | 25 | auto av_error_to_string(int err) -> std::string 26 | { 27 | std::string error_string(AV_ERROR_MAX_STRING_SIZE, '\0'); 28 | if (av_strerror(err, error_string.data(), error_string.size()) < 0) 29 | error_string = "Uknown error"; 30 | return error_string; 31 | } 32 | 33 | CmdLineError::CmdLineError(Type t, std::string const& msg) 34 | : std::runtime_error { msg } 35 | , type { t } 36 | { 37 | } 38 | 39 | } // namespace sc 40 | -------------------------------------------------------------------------------- /src/error.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_ERROR_HPP_INCLUDED 2 | #define SHADOW_CAST_ERROR_HPP_INCLUDED 3 | 4 | #include "av/fwd.hpp" 5 | #include 6 | #include 7 | 8 | namespace sc 9 | { 10 | struct ModuleError final : std::runtime_error 11 | { 12 | ModuleError(std::string const&); 13 | }; 14 | 15 | struct CodecError final : std::runtime_error 16 | { 17 | CodecError(std::string const& msg); 18 | }; 19 | 20 | struct FormatError final : std::runtime_error 21 | { 22 | FormatError(std::string const& msg); 23 | }; 24 | 25 | struct IOError final : std::runtime_error 26 | { 27 | IOError(std::string const& msg); 28 | }; 29 | 30 | auto av_error_to_string(int err) -> std::string; 31 | 32 | struct CmdLineError final : std::runtime_error 33 | { 34 | enum Type 35 | { 36 | show_help, 37 | show_version, 38 | error 39 | }; 40 | 41 | Type type; 42 | 43 | CmdLineError(Type type, std::string const& msg = ""); 44 | }; 45 | 46 | } // namespace sc 47 | 48 | #endif // SHADOW_CAST_ERROR_HPP_INCLUDED 49 | -------------------------------------------------------------------------------- /src/gl/buffer.cpp: -------------------------------------------------------------------------------- 1 | #include "gl/buffer.hpp" 2 | #include "gl/error.hpp" 3 | 4 | namespace sc::opengl 5 | { 6 | auto BufferTraits::create() const -> GLuint 7 | { 8 | GLuint name; 9 | gl().glGenBuffers(1, &name); 10 | SC_CHECK_GL_ERROR("glGenBuffers"); 11 | return name; 12 | } 13 | 14 | auto BufferTraits::destroy(GLuint name) const noexcept -> void 15 | { 16 | gl().glDeleteBuffers(1, &name); 17 | SC_CHECK_GL_ERROR_NOEXCEPT("glDeleteBuffers"); 18 | } 19 | 20 | auto BufferTraits::bind(GLenum target, GLint name) const noexcept -> void 21 | { 22 | gl().glBindBuffer(target, name); 23 | SC_CHECK_GL_ERROR("glBindBuffer"); 24 | } 25 | 26 | auto vertex_attrib_pointer( 27 | BoundTarget, Buffer> const& /*array_buffer*/, 28 | GLuint index, 29 | GLint size, 30 | GLenum type, 31 | GLboolean normalized, 32 | GLsizei stride, 33 | void const* pointer) -> void 34 | { 35 | gl().glVertexAttribPointer(index, size, type, normalized, stride, pointer); 36 | SC_CHECK_GL_ERROR("glVertexAttribPointer"); 37 | } 38 | } // namespace sc::opengl 39 | -------------------------------------------------------------------------------- /src/gl/buffer.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_GL_BUFFER_HPP_INCLUDED 2 | #define SHADOW_CAST_GL_BUFFER_HPP_INCLUDED 3 | 4 | #include "gl/error.hpp" 5 | #include "gl/object.hpp" 6 | #include "platform/opengl.hpp" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | namespace sc 13 | { 14 | 15 | namespace opengl 16 | { 17 | template 18 | concept BufferableConcept = std::is_arithmetic_v; 19 | 20 | struct BufferCategory 21 | { 22 | }; 23 | 24 | struct BufferTraits 25 | { 26 | using category = BufferCategory; 27 | 28 | auto create() const -> GLuint; 29 | auto destroy(GLuint name) const noexcept -> void; 30 | auto bind(GLenum target, GLint name) const noexcept -> void; 31 | }; 32 | 33 | using Buffer = ObjectBase; 34 | template 35 | using BufferTarget = TargetBase; 36 | 37 | template 38 | concept BufferConcept = 39 | std::is_convertible_v; 40 | 41 | template 42 | concept BoundBufferConcept = 43 | BindingConcept> && BufferConcept; 44 | 45 | template 46 | auto named_buffer_data(A& array_buffer, std::span data, GLenum usage) 47 | -> void 48 | { 49 | const auto [id, sz, d] = 50 | std::make_tuple(array_buffer.name(), data.size(), data.data()); 51 | 52 | gl().glNamedBufferData(id, sz * sizeof(T), d, usage); 53 | SC_CHECK_GL_ERROR("glNamedBufferData"); 54 | } 55 | 56 | template 57 | auto buffer_data(A& array_buffer, std::span data, GLenum usage) -> void 58 | { 59 | const auto [target, sz, d] = 60 | std::make_tuple(array_buffer.target(), data.size(), data.data()); 61 | 62 | gl().glBufferData(target, sz * sizeof(T), d, usage); 63 | SC_CHECK_GL_ERROR("glBufferData"); 64 | } 65 | 66 | auto vertex_attrib_pointer( 67 | BoundTarget, Buffer> const& array_buffer, 68 | GLuint index, 69 | GLint size, 70 | GLenum type, 71 | GLboolean normalized, 72 | GLsizei stride, 73 | void const* pointer) -> void; 74 | 75 | constexpr BufferTarget array_buffer_target {}; 76 | constexpr BufferTarget element_array_buffer_target {}; 77 | 78 | } // namespace opengl 79 | 80 | } // namespace sc 81 | 82 | #endif // SHADOW_CAST_GL_BUFFER_HPP_INCLUDED 83 | -------------------------------------------------------------------------------- /src/gl/core.cpp: -------------------------------------------------------------------------------- 1 | #include "gl/core.hpp" 2 | #include "gl/error.hpp" 3 | #include "platform/opengl.hpp" 4 | 5 | namespace sc::opengl 6 | { 7 | 8 | auto viewport(GLint x, GLint y, GLsizei width, GLsizei height) -> void 9 | { 10 | gl().glViewport(x, y, width, height); 11 | SC_CHECK_GL_ERROR("glViewport"); 12 | } 13 | 14 | auto clear(GLenum mask) -> void 15 | { 16 | gl().glClear(mask); 17 | SC_CHECK_GL_ERROR("glClear"); 18 | } 19 | 20 | auto clear_color(float red, float green, float blue, float alpha) noexcept 21 | -> void 22 | { 23 | gl().glClearColor(red, green, blue, alpha); 24 | } 25 | 26 | auto enable(GLenum cap) -> void 27 | { 28 | gl().glEnable(cap); 29 | SC_CHECK_GL_ERROR("glEnable"); 30 | } 31 | 32 | auto blend_function(GLenum sfactor, GLenum dfactor) -> void 33 | { 34 | gl().glBlendFunc(sfactor, dfactor); 35 | SC_CHECK_GL_ERROR("glBlendFunc"); 36 | } 37 | 38 | } // namespace sc::opengl 39 | -------------------------------------------------------------------------------- /src/gl/core.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_GL_CORE_HPP_INCLUDED 2 | #define SHADOW_CAST_GL_CORE_HPP_INCLUDED 3 | 4 | #include 5 | 6 | namespace sc::opengl 7 | { 8 | auto viewport(GLint x, GLint y, GLsizei width, GLsizei height) -> void; 9 | auto clear(GLenum mask) -> void; 10 | auto clear_color(float red, float green, float blue, float alpha) noexcept 11 | -> void; 12 | 13 | auto enable(GLenum cap) -> void; 14 | auto blend_function(GLenum sfactor, GLenum dfactor) -> void; 15 | 16 | } // namespace sc::opengl 17 | 18 | #endif // SHADOW_CAST_GL_CORE_HPP_INCLUDED 19 | -------------------------------------------------------------------------------- /src/gl/error.cpp: -------------------------------------------------------------------------------- 1 | #include "gl/error.hpp" 2 | #include 3 | #include 4 | #include 5 | namespace sc 6 | { 7 | auto gl_error_to_string(GLenum glerror) noexcept -> char const* 8 | { 9 | #define SC_GL_ERROR_STR(val) \ 10 | case val: \ 11 | return "" #val 12 | 13 | switch (glerror) { 14 | SC_GL_ERROR_STR(GL_INVALID_VALUE); 15 | SC_GL_ERROR_STR(GL_INVALID_OPERATION); 16 | SC_GL_ERROR_STR(GL_INVALID_ENUM); 17 | default: 18 | return "GL_UNKNOWN"; 19 | } 20 | 21 | #undef SC_GL_ERROR_STR 22 | } 23 | auto throw_gl_error(std::string const& msg, std::source_location loc) -> void 24 | { 25 | throw std::runtime_error { msg + " " + loc.file_name() + ":" + 26 | std::to_string(loc.line()) }; 27 | } 28 | 29 | namespace opengl 30 | { 31 | auto framebuffer_status_to_string(GLenum glerror) noexcept -> char const* 32 | { 33 | #define SC_GL_FRAMEBUFFER_STATUS_STR(val) \ 34 | case val: \ 35 | return "" #val 36 | 37 | switch (glerror) { 38 | SC_GL_FRAMEBUFFER_STATUS_STR(GL_FRAMEBUFFER_UNDEFINED); 39 | SC_GL_FRAMEBUFFER_STATUS_STR(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT); 40 | SC_GL_FRAMEBUFFER_STATUS_STR( 41 | GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT); 42 | SC_GL_FRAMEBUFFER_STATUS_STR(GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER); 43 | SC_GL_FRAMEBUFFER_STATUS_STR(GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER); 44 | SC_GL_FRAMEBUFFER_STATUS_STR(GL_FRAMEBUFFER_UNSUPPORTED); 45 | SC_GL_FRAMEBUFFER_STATUS_STR(GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE); 46 | SC_GL_FRAMEBUFFER_STATUS_STR(GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS); 47 | default: 48 | return "GL_UNKNOWN"; 49 | } 50 | 51 | #undef SC_GL_FRAMEBUFFER_STATUS_STR 52 | } 53 | } // namespace opengl 54 | 55 | } // namespace sc 56 | -------------------------------------------------------------------------------- /src/gl/error.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_GL_ERROR_HPP_INCLUDED 2 | #define SHADOW_CAST_GL_ERROR_HPP_INCLUDED 3 | 4 | #include "utils/contracts.hpp" 5 | #include 6 | #include 7 | #include 8 | 9 | #define SC_CHECK_GL_ERROR(src) \ 10 | if (auto const glerror = ::sc::gl().glGetError(); glerror != GL_NO_ERROR) \ 11 | ::sc::throw_gl_error(::std::string { src " failed: " } + \ 12 | ::sc::gl_error_to_string(glerror)) 13 | 14 | #define SC_CHECK_GL_ERROR_NOEXCEPT(src) \ 15 | if (auto const glerror = ::sc::gl().glGetError(); glerror != GL_NO_ERROR) \ 16 | ::sc::contract_check_failed(src) 17 | 18 | #define SC_THROW_IF_GL_ERROR SC_CHECK_GL_ERROR 19 | #define SC_ABORT_IF_GL_ERROR SC_CHECK_GL_ERROR_NOEXCEPT 20 | 21 | #if defined(SHADOW_CAST_STRICT_GL_BINDING_CHECK) 22 | #define SC_CHECK_GL_BINDING(binding) \ 23 | if (auto&& bound = binding.is_bound(); !bound) \ 24 | ::sc::contract_check_failed("GL object is not bound") 25 | #else 26 | #define SC_CHECK_GL_BINDING(binding) static_cast(binding) 27 | #endif 28 | 29 | namespace sc 30 | { 31 | [[noreturn]] auto 32 | throw_gl_error(std::string const& msg, 33 | std::source_location = std::source_location::current()) -> void; 34 | auto gl_error_to_string(GLenum) noexcept -> char const*; 35 | 36 | namespace opengl 37 | { 38 | auto framebuffer_status_to_string(GLenum) noexcept -> char const*; 39 | } 40 | } // namespace sc 41 | 42 | #endif // SHADOW_CAST_GL_ERROR_HPP_INCLUDED 43 | -------------------------------------------------------------------------------- /src/gl/framebuffer.cpp: -------------------------------------------------------------------------------- 1 | #include "gl/framebuffer.hpp" 2 | #include "gl/error.hpp" 3 | #include "platform/opengl.hpp" 4 | 5 | namespace sc::opengl 6 | { 7 | auto FramebufferTraits::create() const -> GLuint 8 | { 9 | GLuint name; 10 | gl().glGenFramebuffers(1, &name); 11 | SC_CHECK_GL_ERROR("glGenFramebuffers"); 12 | return name; 13 | } 14 | 15 | auto FramebufferTraits::destroy(GLuint name) const noexcept -> void 16 | { 17 | gl().glDeleteFramebuffers(1, &name); 18 | SC_CHECK_GL_ERROR_NOEXCEPT("glDeleteFramebuffers"); 19 | } 20 | 21 | auto FramebufferTraits::bind(GLenum target, GLuint name) const noexcept -> void 22 | { 23 | gl().glBindFramebuffer(target, name); 24 | SC_CHECK_GL_ERROR_NOEXCEPT("glBindFramebuffer"); 25 | } 26 | 27 | } // namespace sc::opengl 28 | -------------------------------------------------------------------------------- /src/gl/framebuffer.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_GL_FRAMEBUFFER_HPP_INCLUDED 2 | #define SHADOW_CAST_GL_FRAMEBUFFER_HPP_INCLUDED 3 | 4 | #include "gl/error.hpp" 5 | #include "gl/object.hpp" 6 | #include "gl/texture.hpp" 7 | #include "platform/opengl.hpp" 8 | #include 9 | #include 10 | namespace sc::opengl 11 | { 12 | struct FramebufferCategory 13 | { 14 | }; 15 | 16 | struct FramebufferTraits 17 | { 18 | using category = FramebufferCategory; 19 | 20 | auto create() const -> GLuint; 21 | auto destroy(GLuint name) const noexcept -> void; 22 | auto bind(GLenum target, GLuint name) const noexcept -> void; 23 | }; 24 | 25 | using Framebuffer = ObjectBase; 26 | template 27 | using FramebufferTarget = TargetBase; 28 | 29 | template 30 | concept BoundFramebufferConcept = 31 | BindingConcept && 32 | std::is_convertible_v; 33 | 34 | template 35 | auto framebuffer_texture(T const& bound_target, 36 | GLenum attachment, 37 | Texture const& texture, 38 | GLint level) -> void 39 | { 40 | SC_CHECK_GL_BINDING(bound_target); 41 | gl().glFramebufferTexture( 42 | bound_target.target(), attachment, texture.name(), level); 43 | SC_CHECK_GL_ERROR("glFramebufferTexture"); 44 | } 45 | 46 | template 47 | auto get_framebuffer_status(T const& bound_target) -> GLenum 48 | { 49 | SC_CHECK_GL_BINDING(bound_target); 50 | auto const val = gl().glCheckFramebufferStatus(bound_target.target()); 51 | SC_CHECK_GL_ERROR("glCheckFramebufferStatus"); 52 | 53 | return val; 54 | } 55 | 56 | template 57 | auto check_framebuffer_status(T const& bound_target) -> void 58 | { 59 | auto const val = get_framebuffer_status(bound_target); 60 | if (val != GL_FRAMEBUFFER_COMPLETE) 61 | throw_gl_error(framebuffer_status_to_string(val)); 62 | } 63 | 64 | template 65 | auto draw_buffers(T const& bound_target, std::span bufs) -> void 66 | { 67 | SC_CHECK_GL_BINDING(bound_target); 68 | gl().glDrawBuffers(bufs.size(), bufs.data()); 69 | SC_CHECK_GL_ERROR("glDrawBuffers"); 70 | } 71 | 72 | template 73 | auto draw_buffers(T const& bound_target, GLenum arg, Ts... rest) -> void 74 | requires((sizeof...(rest) == 0) || (std::is_same_v && ...)) 75 | { 76 | if constexpr (sizeof...(rest) == 0) { 77 | draw_buffers(bound_target, std::span { &arg, 1 }); 78 | } 79 | else { 80 | std::array const bufs = { arg, rest... }; 81 | draw_buffers(bound_target, 82 | std::span { bufs.data(), bufs.size() }); 83 | } 84 | } 85 | 86 | constexpr FramebufferTarget draw_framebuffer_target {}; 87 | constexpr FramebufferTarget read_framebuffer_target {}; 88 | } // namespace sc::opengl 89 | 90 | #endif // SHADOW_CAST_GL_FRAMEBUFFER_HPP_INCLUDED 91 | -------------------------------------------------------------------------------- /src/gl/gl.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_GL_GL_HPP_INCLUDED 2 | #define SHADOW_CAST_GL_GL_HPP_INCLUDED 3 | 4 | #include "./buffer.hpp" 5 | #include "./core.hpp" 6 | #include "./error.hpp" 7 | #include "./framebuffer.hpp" 8 | #include "./object.hpp" 9 | #include "./program.hpp" 10 | #include "./shader.hpp" 11 | #include "./vertex_array_object.hpp" 12 | 13 | #endif // SHADOW_CAST_GL_GL_HPP_INCLUDED 14 | -------------------------------------------------------------------------------- /src/gl/shader.cpp: -------------------------------------------------------------------------------- 1 | #include "gl/shader.hpp" 2 | #include "gl/error.hpp" 3 | #include "gl/object.hpp" 4 | #include "platform/opengl.hpp" 5 | #include 6 | #include 7 | #include 8 | 9 | namespace 10 | { 11 | thread_local std::array shader_compile_info {}; 12 | } 13 | 14 | namespace sc 15 | { 16 | 17 | namespace opengl 18 | { 19 | auto ShaderTraits::create(GLenum shader_type) const -> GLuint 20 | { 21 | auto const name = gl().glCreateShader(shader_type); 22 | SC_CHECK_GL_ERROR("glCreateShader"); 23 | return name; 24 | } 25 | 26 | auto ShaderTraits::destroy(GLuint name) const noexcept -> void 27 | { 28 | gl().glDeleteShader(name); 29 | SC_CHECK_GL_ERROR_NOEXCEPT("glDeleteShader"); 30 | } 31 | 32 | auto create_shader(ShaderType type) -> Shader 33 | { 34 | switch (type) { 35 | case ShaderType::vertex: 36 | return create(GL_VERTEX_SHADER); 37 | case ShaderType::fragment: 38 | default: 39 | return create(GL_FRAGMENT_SHADER); 40 | } 41 | } 42 | 43 | auto shader_source(Shader& shader, std::string_view src) -> void 44 | { 45 | GLint const length = static_cast(src.size()); 46 | char const* data = src.data(); 47 | gl().glShaderSource(shader.name(), 1, &data, &length); 48 | SC_CHECK_GL_ERROR("glShaderSource"); 49 | } 50 | 51 | auto compile_shader(Shader& shader) -> void 52 | { 53 | gl().glCompileShader(shader.name()); 54 | SC_CHECK_GL_ERROR("glCompileShader"); 55 | 56 | GLint compile_status; 57 | gl().glGetShaderiv(shader.name(), GL_COMPILE_STATUS, &compile_status); 58 | SC_CHECK_GL_ERROR("glGetShaderiv"); 59 | 60 | if (compile_status != GL_TRUE) { 61 | GLsizei compile_log_length; 62 | gl().glGetShaderInfoLog(shader.name(), 63 | shader_compile_info.size(), 64 | &compile_log_length, 65 | shader_compile_info.data()); 66 | SC_CHECK_GL_ERROR("glGetShaderInfoLog"); 67 | 68 | sc::throw_gl_error( 69 | "Shader compilation failed - " + 70 | std::string(shader_compile_info.data(), compile_log_length)); 71 | } 72 | } 73 | 74 | } // namespace opengl 75 | 76 | } // namespace sc 77 | -------------------------------------------------------------------------------- /src/gl/shader.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_GL_SHADER_HPP_INCLUDED 2 | #define SHADOW_CAST_GL_SHADER_HPP_INCLUDED 3 | 4 | #include "gl/object.hpp" 5 | #include 6 | #include 7 | 8 | namespace sc 9 | { 10 | namespace opengl 11 | { 12 | 13 | struct ShaderCategory 14 | { 15 | }; 16 | 17 | enum class ShaderType 18 | { 19 | vertex, 20 | fragment, 21 | }; 22 | 23 | struct ShaderTraits 24 | { 25 | using category = ShaderCategory; 26 | 27 | auto create(GLenum shader_type) const -> GLuint; 28 | auto destroy(GLuint name) const noexcept -> void; 29 | }; 30 | 31 | using Shader = ObjectBase; 32 | 33 | auto create_shader(ShaderType type) -> Shader; 34 | auto shader_source(Shader& shader, std::string_view) -> void; 35 | auto compile_shader(Shader& shader) -> void; 36 | 37 | } // namespace opengl 38 | 39 | } // namespace sc 40 | #endif // SHADOW_CAST_GL_SHADER_HPP_INCLUDED 41 | -------------------------------------------------------------------------------- /src/gl/texture.cpp: -------------------------------------------------------------------------------- 1 | #include "gl/texture.hpp" 2 | #include "gl/error.hpp" 3 | #include "platform/opengl.hpp" 4 | #include "utils/contracts.hpp" 5 | #include 6 | #include 7 | 8 | namespace sc::opengl 9 | { 10 | 11 | auto TextureTraits::bind(GLenum target, GLuint name) const noexcept -> void 12 | { 13 | gl().glBindTexture(target, name); 14 | SC_CHECK_GL_ERROR_NOEXCEPT("glBindTexture"); 15 | } 16 | 17 | auto TextureTraits::create() const -> GLuint 18 | { 19 | GLuint name; 20 | gl().glGenTextures(1, &name); 21 | SC_CHECK_GL_ERROR("glGenTextures"); 22 | return name; 23 | } 24 | 25 | auto TextureTraits::destroy(GLuint name) const noexcept -> void 26 | { 27 | gl().glDeleteTextures(1, &name); 28 | SC_CHECK_GL_ERROR_NOEXCEPT("glDeleteTextures"); 29 | } 30 | 31 | } // namespace sc::opengl 32 | -------------------------------------------------------------------------------- /src/gl/vertex_array_object.cpp: -------------------------------------------------------------------------------- 1 | #include "gl/vertex_array_object.hpp" 2 | #include "gl/error.hpp" 3 | #include "gl/program.hpp" 4 | #include "platform/opengl.hpp" 5 | 6 | namespace sc 7 | { 8 | namespace opengl 9 | { 10 | auto VertexArrayTraits::create() const -> GLuint 11 | { 12 | GLuint name; 13 | gl().glGenVertexArrays(1, &name); 14 | SC_CHECK_GL_ERROR("glGenVertexArrays"); 15 | return name; 16 | } 17 | 18 | auto VertexArrayTraits::destroy(GLuint name) const noexcept -> void 19 | { 20 | gl().glDeleteVertexArrays(1, &name); 21 | SC_CHECK_GL_ERROR_NOEXCEPT("glDeleteVertexArrays"); 22 | } 23 | 24 | auto VertexArrayTraits::bind(GLenum /*target*/, GLuint name) const noexcept 25 | -> void 26 | { 27 | gl().glBindVertexArray(name); 28 | SC_CHECK_GL_ERROR_NOEXCEPT("glBindVertexArray"); 29 | } 30 | 31 | auto enable_vertex_array_attrib(BoundVertexArray& vertex_array, GLuint index) 32 | -> void 33 | { 34 | gl().glEnableVertexArrayAttrib(vertex_array.name(), index); 35 | SC_CHECK_GL_ERROR("glEnableVertexArrayAttrib"); 36 | } 37 | 38 | auto disable_vertex_array_attrib(BoundVertexArray& vertex_array, GLuint index) 39 | -> void 40 | { 41 | gl().glDisableVertexArrayAttrib(vertex_array.name(), index); 42 | SC_CHECK_GL_ERROR("glDisableVertexArrayAttrib"); 43 | } 44 | 45 | auto draw_arrays(BoundVertexArray const&, 46 | BoundProgram const& bound_program, 47 | GLenum mode, 48 | GLint first, 49 | GLsizei count) -> void 50 | { 51 | SC_CHECK_GL_BINDING(bound_program); 52 | gl().glDrawArrays(mode, first, count); 53 | SC_CHECK_GL_ERROR("glDrawArrays"); 54 | } 55 | 56 | auto draw_elements(BoundVertexArray const& bound_target, 57 | BoundTarget, 58 | Buffer> const& bound_element_array, 59 | BoundProgram const& bound_program, 60 | GLenum mode, 61 | GLsizei count, 62 | GLenum type, 63 | const void* indices) -> void 64 | { 65 | SC_CHECK_GL_BINDING(bound_target); 66 | SC_CHECK_GL_BINDING(bound_element_array); 67 | SC_CHECK_GL_BINDING(bound_program); 68 | gl().glDrawElements(mode, count, type, indices); 69 | SC_CHECK_GL_ERROR("glDrawElements"); 70 | } 71 | 72 | } // namespace opengl 73 | 74 | } // namespace sc 75 | -------------------------------------------------------------------------------- /src/gl/vertex_array_object.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_GL_VERTEX_ARRAY_OBJECT_HPP_INCLUDED 2 | #define SHADOW_CAST_GL_VERTEX_ARRAY_OBJECT_HPP_INCLUDED 3 | 4 | #include "gl/buffer.hpp" 5 | #include "gl/object.hpp" 6 | #include "gl/program.hpp" 7 | 8 | namespace sc 9 | { 10 | namespace opengl 11 | { 12 | struct VertexArrayCategory 13 | { 14 | }; 15 | 16 | struct VertexArrayTraits 17 | { 18 | using category = VertexArrayCategory; 19 | auto create() const -> GLuint; 20 | auto destroy(GLuint name) const noexcept -> void; 21 | auto bind(GLenum /*target*/, GLuint name) const noexcept -> void; 22 | }; 23 | 24 | using VertexArray = ObjectBase; 25 | using VertexArrayTarget = TargetBase<0, VertexArrayTraits>; 26 | using BoundVertexArray = BoundTarget; 27 | constexpr VertexArrayTarget vertex_array_target {}; 28 | 29 | auto enable_vertex_array_attrib(BoundVertexArray& vertex_array, GLuint index) 30 | -> void; 31 | auto disable_vertex_array_attrib(BoundVertexArray& vertex_array, GLuint index) 32 | -> void; 33 | auto draw_arrays(BoundVertexArray const& vertex_array, 34 | BoundProgram const& program, 35 | GLenum mode, 36 | GLint first, 37 | GLsizei count) -> void; 38 | 39 | auto draw_elements(BoundVertexArray const& bound_target, 40 | BoundTarget, 41 | Buffer> const& bound_element_array, 42 | BoundProgram const& program, 43 | GLenum mode, 44 | GLsizei count, 45 | GLenum type, 46 | const void* indices) -> void; 47 | } // namespace opengl 48 | 49 | } // namespace sc 50 | 51 | #endif // SHADOW_CAST_GL_VERTEX_ARRAY_OBJECT_HPP_INCLUDED 52 | -------------------------------------------------------------------------------- /src/glsl/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(EmbeddedGLSLTarget) 2 | 3 | add_embedded_glsl_target( 4 | NAME embedded_glsl 5 | SOURCES 6 | default_vertex.glsl 7 | default_fragment.glsl 8 | mouse_vertex.glsl 9 | mouse_fragment.glsl 10 | ) 11 | -------------------------------------------------------------------------------- /src/glsl/default_fragment.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | #extension GL_OES_EGL_image_external : enable 3 | 4 | out vec4 FragColor; 5 | in vec2 tex_coord; 6 | uniform samplerExternalOES texture_sampler; 7 | 8 | void main() 9 | { 10 | FragColor = vec4(texture2D(texture_sampler, tex_coord).rgb, 1.0); 11 | } 12 | 13 | // vim: ft=glsl 14 | -------------------------------------------------------------------------------- /src/glsl/default_vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | layout (location = 0) in vec3 aPos; 3 | layout (location = 1) in vec2 aTexCoord; 4 | 5 | out vec2 tex_coord; 6 | 7 | void main() 8 | { 9 | gl_Position = vec4(aPos.xyz, 1.0); 10 | tex_coord = aTexCoord; 11 | } 12 | 13 | // vim: ft=glsl 14 | -------------------------------------------------------------------------------- /src/glsl/mouse_fragment.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | #extension GL_OES_EGL_image_external : enable 3 | 4 | out vec4 FragColor; 5 | in vec2 tex_coord; 6 | uniform samplerExternalOES texture_sampler; 7 | 8 | void main() 9 | { 10 | FragColor = texture2D(texture_sampler, tex_coord); 11 | } 12 | 13 | // vim: ft=glsl 14 | -------------------------------------------------------------------------------- /src/glsl/mouse_vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | layout (location = 0) in vec3 aPos; 3 | layout (location = 1) in vec2 aTexCoord; 4 | 5 | uniform vec2 screen_dimensions; 6 | uniform vec2 mouse_dimensions; 7 | uniform vec2 mouse_position; 8 | 9 | out vec2 tex_coord; 10 | 11 | void main() 12 | { 13 | /* NOTE: 14 | * We must scale all the mouse plane's dimensions and screen position 15 | * to -1.0,1.0 coords... 16 | * - First scale the mouse plane to the screen scale. 17 | * - Then offset the mouse position by _adding_ half its w/h, 18 | * and _subtracting_ half the screen w/h. 19 | * - Finally translate its x/y position by dividing by half screen 20 | * w/h. 21 | */ 22 | 23 | /* NOTE: 24 | * We don't have to worry about inverting the y coords because the 25 | * texture will render upside down (which is correct when we finally 26 | * copy it to the encoder stage). 27 | */ 28 | 29 | vec2 scaled_dimensions = mouse_dimensions / screen_dimensions; 30 | mat4 scale; 31 | scale[0] = vec4(scaled_dimensions.x, 0.0, 0.0, 0.0); 32 | scale[1] = vec4(0.0, scaled_dimensions.y, 0.0, 0.0); 33 | scale[2] = vec4(0.0, 0.0, 1.0, 0.0); 34 | scale[3] = vec4(0.0, 0.0, 0.0, 1.0); 35 | 36 | vec2 half_screen = screen_dimensions / 2; 37 | 38 | vec2 mpos = (mouse_position + mouse_dimensions / 2) - half_screen; 39 | 40 | mat4 translate; 41 | translate[0] = vec4(1.0, 0.0, 0.0, 0.0); 42 | translate[1] = vec4(0.0, 1.0, 0.0, 0.0); 43 | translate[2] = vec4(0.0, 0.0, 1.0, 0.0); 44 | translate[3] = vec4(mpos / half_screen, 0.0, 1.0); 45 | 46 | mat4 mv = translate * scale; 47 | 48 | gl_Position = mv * vec4(aPos, 1.0); 49 | tex_coord = aTexCoord; 50 | } 51 | 52 | // vim: ft=glsl 53 | -------------------------------------------------------------------------------- /src/handlers.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_HANDLERS_HPP_INCLUDED 2 | #define SHADOW_CAST_HANDLERS_HPP_INCLUDED 3 | 4 | #include "./handlers/audio_chunk_writer.hpp" 5 | #include "./handlers/drm_video_frame_writer.hpp" 6 | #include "./handlers/stream_finalizer.hpp" 7 | #include "./handlers/video_frame_writer.hpp" 8 | 9 | #endif // SHADOW_CAST_HANDLERS_HPP_INCLUDED 10 | -------------------------------------------------------------------------------- /src/handlers/audio_chunk_writer.cpp: -------------------------------------------------------------------------------- 1 | #include "handlers/audio_chunk_writer.hpp" 2 | #include "av/frame.hpp" 3 | #include "av/sample_format.hpp" 4 | #include "config.hpp" 5 | #include "error.hpp" 6 | #include "services/encoder.hpp" 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace sc 13 | { 14 | 15 | ChunkWriter::ChunkWriter(AVCodecContext* codec_context, 16 | AVStream* stream, 17 | Encoder encoder, 18 | std::size_t frame_size) noexcept 19 | : codec_context_ { codec_context } 20 | , stream_ { stream } 21 | , encoder_ { encoder } 22 | , frame_size_ { frame_size } 23 | , frame_ { av_frame_alloc() } 24 | , total_samples_written_ { 0 } 25 | { 26 | } 27 | 28 | auto ChunkWriter::operator()(MediaChunk const& chunk) -> void 29 | { 30 | SC_EXPECT(chunk.sample_count >= frame_size_); 31 | sc::SampleFormat const sample_format = 32 | sc::convert_from_libav_format(codec_context_->sample_fmt); 33 | 34 | auto const sample_size = sc::sample_format_size(sample_format); 35 | auto const interleaved = sc::is_interleaved_format(sample_format); 36 | 37 | auto encoder_frame = 38 | encoder_.prepare_frame(codec_context_.get(), stream_.get()); 39 | auto* frame = encoder_frame->frame.get(); 40 | 41 | frame->nb_samples = frame_size_; 42 | frame->format = codec_context_->sample_fmt; 43 | frame->sample_rate = codec_context_->sample_rate; 44 | #if LIBAVCODEC_VERSION_MAJOR < 60 45 | frame->channels = interleaved ? 2 : chunk.channel_buffers().size(); 46 | frame->channel_layout = AV_CH_LAYOUT_STEREO; 47 | #else 48 | av_channel_layout_copy(&frame->ch_layout, &codec_context_->ch_layout); 49 | #endif 50 | frame->pts = total_samples_written_; 51 | total_samples_written_ += frame->nb_samples; 52 | 53 | sc::initialize_writable_buffer(frame); 54 | 55 | auto n = 0; 56 | for (auto const& channel_buffer : chunk.channel_buffers()) { 57 | std::size_t const num_bytes = interleaved 58 | ? frame->nb_samples * sample_size * 2 59 | : frame->nb_samples * sample_size; 60 | 61 | std::span source = channel_buffer.data().subspan(0, num_bytes); 62 | std::span target { reinterpret_cast(frame->data[n++]), 63 | num_bytes }; 64 | 65 | std::copy(begin(source), end(source), begin(target)); 66 | } 67 | 68 | encoder_.write_frame(std::move(encoder_frame)); 69 | } 70 | 71 | } // namespace sc 72 | -------------------------------------------------------------------------------- /src/handlers/audio_chunk_writer.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_HANDLERS_AUDIO_CHUNK_WRITER_HPP_INCLUDED 2 | #define SHADOW_CAST_HANDLERS_AUDIO_CHUNK_WRITER_HPP_INCLUDED 3 | 4 | #include "av.hpp" 5 | #include "services/encoder.hpp" 6 | #include "utils/borrowed_ptr.hpp" 7 | 8 | namespace sc 9 | { 10 | 11 | struct ChunkWriter 12 | { 13 | explicit ChunkWriter(AVCodecContext* codec_context, 14 | AVStream* stream, 15 | Encoder encoder, 16 | std::size_t frame_size) noexcept; 17 | 18 | auto operator()(MediaChunk const& chunk) -> void; 19 | 20 | private: 21 | BorrowedPtr codec_context_; 22 | BorrowedPtr stream_; 23 | Encoder encoder_; 24 | std::size_t frame_size_; 25 | FramePtr frame_; 26 | std::size_t total_samples_written_ { 0 }; 27 | }; 28 | 29 | } // namespace sc 30 | 31 | #endif // SHADOW_CAST_HANDLERS_AUDIO_CHUNK_WRITER_HPP_INCLUDED 32 | -------------------------------------------------------------------------------- /src/handlers/drm_video_frame_writer.cpp: -------------------------------------------------------------------------------- 1 | #include "handlers/drm_video_frame_writer.hpp" 2 | #include "services/encoder.hpp" 3 | #include "utils/elapsed.hpp" 4 | 5 | namespace sc 6 | { 7 | 8 | DRMVideoFrameWriter::DRMVideoFrameWriter(AVCodecContext* codec_context, 9 | AVStream* stream, 10 | Encoder encoder) 11 | : codec_context_ { codec_context } 12 | , stream_ { stream } 13 | , encoder_ { encoder } 14 | { 15 | } 16 | 17 | auto DRMVideoFrameWriter::operator()(CUarray data, 18 | NvCuda const& cuda, 19 | std::uint64_t /*frame_time*/) -> void 20 | { 21 | SC_EXPECT(data); 22 | 23 | auto encoder_frame = 24 | encoder_.prepare_frame(codec_context_.get(), stream_.get()); 25 | auto* frame = encoder_frame->frame.get(); 26 | 27 | frame->format = codec_context_->pix_fmt; 28 | frame->width = codec_context_->width; 29 | frame->height = codec_context_->height; 30 | frame->color_range = codec_context_->color_range; 31 | frame->color_primaries = codec_context_->color_primaries; 32 | frame->color_trc = codec_context_->color_trc; 33 | frame->colorspace = codec_context_->colorspace; 34 | frame->chroma_location = codec_context_->chroma_sample_location; 35 | if (auto const r = 36 | av_hwframe_get_buffer(codec_context_->hw_frames_ctx, frame, 0); 37 | r < 0) 38 | throw std::runtime_error { "Failed to get H/W frame buffer" }; 39 | 40 | SC_EXPECT(frame->linesize[0]); 41 | SC_EXPECT(frame->height); 42 | SC_EXPECT(frame->data[0]); 43 | 44 | CUDA_MEMCPY2D memcpy_struct {}; 45 | 46 | memcpy_struct.srcXInBytes = 0; 47 | memcpy_struct.srcY = 0; 48 | memcpy_struct.srcMemoryType = CU_MEMORYTYPE_ARRAY; 49 | memcpy_struct.dstXInBytes = 0; 50 | memcpy_struct.dstY = 0; 51 | memcpy_struct.dstMemoryType = CU_MEMORYTYPE_DEVICE; 52 | memcpy_struct.srcArray = data; 53 | memcpy_struct.dstDevice = reinterpret_cast(frame->data[0]); 54 | memcpy_struct.dstPitch = frame->linesize[0]; 55 | memcpy_struct.WidthInBytes = frame->linesize[0]; 56 | memcpy_struct.Height = frame->height; 57 | 58 | if (auto const r = cuda.cuMemcpy2D_v2(&memcpy_struct); r != CUDA_SUCCESS) { 59 | char const* err = "unknown"; 60 | cuda.cuGetErrorString(r, &err); 61 | throw std::runtime_error { 62 | std::to_string(frame_number_) + 63 | std::string { " Failed to copy CUDA buffer: " } + err 64 | }; 65 | } 66 | 67 | frame->pts = frame_number_++; 68 | 69 | encoder_.write_frame(std::move(encoder_frame)); 70 | } 71 | 72 | } // namespace sc 73 | -------------------------------------------------------------------------------- /src/handlers/drm_video_frame_writer.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_HANDLERS_DRM_VIDEO_FRAME_WRITER_HPP_INCLUDED 2 | #define SHADOW_CAST_HANDLERS_DRM_VIDEO_FRAME_WRITER_HPP_INCLUDED 3 | 4 | #include "av.hpp" 5 | #include "nvidia.hpp" 6 | #include "services/encoder.hpp" 7 | #include 8 | 9 | namespace sc 10 | { 11 | struct DRMVideoFrameWriter 12 | { 13 | DRMVideoFrameWriter(AVCodecContext* codec_context, 14 | AVStream* stream, 15 | Encoder encoder); 16 | 17 | auto operator()(CUarray, NvCuda const&, std::uint64_t) -> void; 18 | 19 | private: 20 | BorrowedPtr codec_context_; 21 | BorrowedPtr stream_; 22 | Encoder encoder_; 23 | std::size_t frame_number_ { 0 }; 24 | }; 25 | 26 | } // namespace sc 27 | 28 | #endif // SHADOW_CAST_HANDLERS_DRM_VIDEO_FRAME_WRITER_HPP_INCLUDED 29 | -------------------------------------------------------------------------------- /src/handlers/stream_finalizer.cpp: -------------------------------------------------------------------------------- 1 | #include "handlers/stream_finalizer.hpp" 2 | #include "av/frame.hpp" 3 | #include "error.hpp" 4 | #include 5 | 6 | namespace sc 7 | { 8 | 9 | StreamFinalizer::StreamFinalizer( 10 | BorrowedPtr format_context, 11 | BorrowedPtr audio_codec_context, 12 | BorrowedPtr video_codec_context, 13 | BorrowedPtr audio_stream, 14 | BorrowedPtr video_stream) 15 | : format_context_ { format_context } 16 | , audio_codec_context_ { audio_codec_context } 17 | , video_codec_context_ { video_codec_context } 18 | , audio_stream_ { audio_stream } 19 | , video_stream_ { video_stream } 20 | , packet_ { av_packet_alloc() } 21 | { 22 | } 23 | 24 | auto StreamFinalizer::operator()() const -> void 25 | { 26 | /* Send a end marker to the audio stream... 27 | */ 28 | send_frame(nullptr, 29 | audio_codec_context_.get(), 30 | format_context_.get(), 31 | audio_stream_.get(), 32 | packet_.get()); 33 | 34 | /* Hijack this handler to send an end marker to the video stream, too... 35 | */ 36 | send_frame(nullptr, 37 | video_codec_context_.get(), 38 | format_context_.get(), 39 | video_stream_.get(), 40 | packet_.get()); 41 | } 42 | } // namespace sc 43 | -------------------------------------------------------------------------------- /src/handlers/stream_finalizer.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_HANDLERS_STREAM_FINALIZER_HPP_INCLUDED 2 | #define SHADOW_CAST_HANDLERS_STREAM_FINALIZER_HPP_INCLUDED 3 | 4 | #include "av/fwd.hpp" 5 | #include "av/packet.hpp" 6 | #include "utils/borrowed_ptr.hpp" 7 | 8 | namespace sc 9 | { 10 | 11 | struct StreamFinalizer 12 | { 13 | StreamFinalizer(BorrowedPtr format_context, 14 | BorrowedPtr audio_codec_context, 15 | BorrowedPtr video_codec_context, 16 | BorrowedPtr audio_stream, 17 | BorrowedPtr video_stream); 18 | 19 | auto operator()() const -> void; 20 | 21 | private: 22 | BorrowedPtr format_context_; 23 | BorrowedPtr audio_codec_context_; 24 | BorrowedPtr video_codec_context_; 25 | BorrowedPtr audio_stream_; 26 | BorrowedPtr video_stream_; 27 | PacketPtr packet_; 28 | }; 29 | 30 | } // namespace sc 31 | 32 | #endif // SHADOW_CAST_HANDLERS_STREAM_FINALIZER_HPP_INCLUDED 33 | -------------------------------------------------------------------------------- /src/handlers/video_frame_writer.cpp: -------------------------------------------------------------------------------- 1 | #include "handlers/video_frame_writer.hpp" 2 | #include "handlers/audio_chunk_writer.hpp" 3 | #include "services/encoder.hpp" 4 | #include "utils/elapsed.hpp" 5 | 6 | namespace sc 7 | { 8 | 9 | VideoFrameWriter::VideoFrameWriter(AVCodecContext* codec_context, 10 | AVStream* stream, 11 | Encoder encoder) 12 | : codec_context_ { codec_context } 13 | , stream_ { stream } 14 | , encoder_ { encoder } 15 | { 16 | } 17 | 18 | auto VideoFrameWriter::operator()(CUdeviceptr cu_device_ptr, 19 | NVFBC_FRAME_GRAB_INFO, 20 | std::uint64_t /*frame_time*/) -> void 21 | { 22 | auto encoder_frame = 23 | encoder_.prepare_frame(codec_context_.get(), stream_.get()); 24 | auto* frame = encoder_frame->frame.get(); 25 | 26 | frame->hw_frames_ctx = av_buffer_ref(codec_context_->hw_frames_ctx); 27 | 28 | auto hw_frames_ctx = reinterpret_cast( 29 | codec_context_->hw_frames_ctx->data); 30 | frame->buf[0] = av_buffer_pool_get(hw_frames_ctx->pool); 31 | 32 | frame->data[0] = reinterpret_cast(cu_device_ptr); 33 | frame->linesize[0] = codec_context_->width * sizeof(std::uint32_t); 34 | 35 | frame->extended_data = frame->data; 36 | frame->format = codec_context_->pix_fmt; 37 | frame->width = codec_context_->width; 38 | frame->height = codec_context_->height; 39 | frame->color_range = codec_context_->color_range; 40 | frame->color_primaries = codec_context_->color_primaries; 41 | frame->color_trc = codec_context_->color_trc; 42 | frame->colorspace = codec_context_->colorspace; 43 | frame->chroma_location = codec_context_->chroma_sample_location; 44 | 45 | frame->pts = frame_number_++; 46 | 47 | encoder_.write_frame(std::move(encoder_frame)); 48 | } 49 | 50 | } // namespace sc 51 | -------------------------------------------------------------------------------- /src/handlers/video_frame_writer.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_HANDLERS_VIDEO_FRAME_WRITER_HPP_INCLUDED 2 | #define SHADOW_CAST_HANDLERS_VIDEO_FRAME_WRITER_HPP_INCLUDED 3 | 4 | #include "av.hpp" 5 | #include "nvidia.hpp" 6 | #include "services/encoder.hpp" 7 | #include 8 | 9 | namespace sc 10 | { 11 | struct VideoFrameWriter 12 | { 13 | VideoFrameWriter(AVCodecContext* codec_context, 14 | AVStream* stream, 15 | Encoder encoder); 16 | 17 | auto operator()(CUdeviceptr cu_device_ptr, 18 | NVFBC_FRAME_GRAB_INFO, 19 | std::uint64_t) -> void; 20 | 21 | private: 22 | BorrowedPtr codec_context_; 23 | BorrowedPtr stream_; 24 | Encoder encoder_; 25 | std::size_t frame_number_ { 0 }; 26 | }; 27 | 28 | } // namespace sc 29 | #endif // SHADOW_CAST_HANDLERS_VIDEO_FRAME_WRITER_HPP_INCLUDED 30 | -------------------------------------------------------------------------------- /src/io.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_IO_HPP_INCLUDED 2 | #define SHADOW_CAST_IO_HPP_INCLUDED 3 | 4 | #include "./io/accept_handler.hpp" 5 | #include "./io/message_receiver.hpp" 6 | #include "./io/message_sender.hpp" 7 | #include "./io/process.hpp" 8 | #include "./io/signals.hpp" 9 | #include "./io/unix_socket.hpp" 10 | 11 | #endif // SHADOW_CAST_IO_HPP_INCLUDED 12 | -------------------------------------------------------------------------------- /src/io/accept_handler.cpp: -------------------------------------------------------------------------------- 1 | #include "io/accept_handler.hpp" 2 | #include 3 | #include 4 | 5 | namespace sc 6 | { 7 | AcceptHandler::AcceptHandler(std::size_t tout, sigset_t* mask) noexcept 8 | : timeout_ms_ { tout } 9 | , sigmask_ { mask } 10 | { 11 | } 12 | 13 | auto AcceptHandler::operator()(int fd) noexcept -> Result 14 | { 15 | pollfd pfd[1] = {}; 16 | pfd[0].fd = fd; 17 | pfd[0].events = POLLIN; 18 | 19 | timespec ts_storage = {}; 20 | timespec* ts = nullptr; 21 | if (timeout_ms_ != timeout::kInfinite) { 22 | ts_storage = { .tv_sec = static_cast(timeout_ms_ / 1'000), 23 | .tv_nsec = static_cast((timeout_ms_ % 1'000) * 24 | 1'000'000) }; 25 | ts = &ts_storage; 26 | } 27 | 28 | /* NOTE: 29 | * sigmask_ should be the *INVERSE* of the signals that we 30 | * want to be notified on. E.g. if we're currently blocking 31 | * SIGINT, and we wish for ppoll to tell us if a SIGINT 32 | * occurred, then `sigmask_` *MUST NOT* contain SIGINT. 33 | * 34 | * Essentially, the current mask will be *SET* to `sigmask_` 35 | * for the duration of `ppoll()`, and will be restored to 36 | * its previous value after the call. If a signal occurs 37 | * the `ppoll()` will return `EINTR`... 38 | */ 39 | auto const poll_result = ::ppoll(pfd, 1, ts, sigmask_); 40 | 41 | if (poll_result < 0) 42 | return result_error(std::error_code { errno, std::system_category() }); 43 | 44 | if (poll_result == 0) 45 | return result_error( 46 | std::error_code { ETIMEDOUT, std::system_category() }); 47 | 48 | sockaddr_un remote_addr; 49 | socklen_t len = sizeof(sockaddr_un); 50 | auto const accept_result = 51 | ::accept(fd, reinterpret_cast(&remote_addr), &len); 52 | if (accept_result < 0) 53 | return result_error(std::error_code { errno, std::system_category() }); 54 | 55 | return result_ok(accept_result); 56 | } 57 | 58 | } // namespace sc 59 | -------------------------------------------------------------------------------- /src/io/accept_handler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_IO_ACCEPT_HANDLE_HPP_INCLUDED 2 | #define SHADOW_CAST_IO_ACCEPT_HANDLE_HPP_INCLUDED 3 | 4 | #include "io/message_handler.hpp" 5 | #include 6 | #include 7 | #include 8 | 9 | namespace sc 10 | { 11 | struct AcceptHandler 12 | { 13 | explicit AcceptHandler(std::size_t /*timeout_ms*/ = timeout::kInfinite, 14 | sigset_t* /*sigmask*/ = nullptr) noexcept; 15 | 16 | auto operator()(int /*fd*/) noexcept -> Result; 17 | 18 | private: 19 | std::size_t timeout_ms_; 20 | sigset_t* sigmask_; 21 | }; 22 | } // namespace sc 23 | #endif // SHADOW_CAST_IO_ACCEPT_HANDLE_HPP_INCLUDED 24 | -------------------------------------------------------------------------------- /src/io/message_handler.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_IO_MESSAGE_HANDLER_HPP_INCLUDED 2 | #define SHADOW_CAST_IO_MESSAGE_HANDLER_HPP_INCLUDED 3 | 4 | #include "utils/contracts.hpp" 5 | #include "utils/result.hpp" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace sc 16 | { 17 | 18 | namespace timeout 19 | { 20 | std::size_t constexpr kInfinite = static_cast(-1); 21 | } 22 | 23 | namespace io 24 | { 25 | constexpr struct 26 | { 27 | } read; 28 | constexpr struct 29 | { 30 | } write; 31 | } // namespace io 32 | 33 | template 34 | requires(std::is_same_v || 35 | std::is_same_v) 36 | struct MessageHandler 37 | { 38 | 39 | explicit MessageHandler(T& payload, 40 | std::size_t timeout_ms = timeout::kInfinite, 41 | sigset_t* sigmask = nullptr) noexcept 42 | : payload_ { &payload } 43 | , timeout_ms_ { timeout_ms } 44 | , sigmask_ { sigmask } 45 | { 46 | } 47 | 48 | auto operator()(int fd) noexcept -> Result 49 | { 50 | iovec iov = { payload_, sizeof(T) }; 51 | msghdr msg {}; 52 | msg.msg_iov = &iov; 53 | msg.msg_iovlen = 1; 54 | 55 | pollfd pfd {}; 56 | pfd.fd = fd; 57 | if constexpr (std::is_same_v) { 58 | pfd.events = POLLIN; 59 | } 60 | else { 61 | pfd.events = POLLOUT; 62 | } 63 | 64 | timespec ts_storage = {}; 65 | timespec* ts = nullptr; 66 | if (timeout_ms_ != timeout::kInfinite) { 67 | ts_storage = { .tv_sec = static_cast(timeout_ms_ / 1'000), 68 | .tv_nsec = static_cast( 69 | (timeout_ms_ % 1'000) * 1'000'000) }; 70 | ts = &ts_storage; 71 | } 72 | 73 | /* NOTE: 74 | * sigmask_ should be the *INVERSE* of the signals that we 75 | * want to be notified on. E.g. if we're currently blocking 76 | * SIGINT, and we wish for ppoll to tell us if a SIGINT 77 | * occurred, then `sigmask_` *MUST NOT* contain SIGINT. 78 | * 79 | * Essentially, the current mask will be *SET* to `sigmask_` 80 | * for the duration of `ppoll()`, and will be restored to 81 | * its previous value after the call. If a signal occurs 82 | * the `ppoll()` will return `EINTR`... 83 | */ 84 | auto const poll_result = ::ppoll(&pfd, 1, ts, sigmask_); 85 | 86 | if (poll_result < 0) 87 | return result_error( 88 | std::error_code { errno, std::system_category() }); 89 | 90 | if (poll_result == 0) 91 | return result_ok(0ull); 92 | 93 | auto const msg_result = handler_(fd, msg, *payload_); 94 | if (msg_result < 0) 95 | return result_error( 96 | std::error_code { errno, std::system_category() }); 97 | 98 | return result_ok(static_cast(msg_result)); 99 | } 100 | 101 | private: 102 | Handler handler_ {}; 103 | T* payload_; 104 | std::size_t timeout_ms_ { timeout::kInfinite }; 105 | sigset_t* sigmask_ { nullptr }; 106 | }; 107 | 108 | } // namespace sc 109 | 110 | #endif // SHADOW_CAST_IO_MESSAGE_HANDLER_HPP_INCLUDED 111 | -------------------------------------------------------------------------------- /src/io/message_receiver.cpp: -------------------------------------------------------------------------------- 1 | #include "io/message_receiver.hpp" 2 | -------------------------------------------------------------------------------- /src/io/message_receiver.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_IO_MESSAGE_RECEIVER_HPP_INCLUDED 2 | #define SHADOW_CAST_IO_MESSAGE_RECEIVER_HPP_INCLUDED 3 | 4 | #include "io/message_handler.hpp" 5 | 6 | namespace sc 7 | { 8 | 9 | template 10 | struct ReceiveHandler 11 | { 12 | auto operator()(int fd, msghdr& msg, T&) noexcept -> ssize_t 13 | { 14 | return ::recvmsg(fd, &msg, MSG_WAITALL); 15 | } 16 | }; 17 | 18 | template 19 | using MessageReceiver = 20 | MessageHandler, decltype(io::read)>; 21 | 22 | } // namespace sc 23 | 24 | #endif // SHADOW_CAST_IO_MESSAGE_RECEIVER_HPP_INCLUDED 25 | -------------------------------------------------------------------------------- /src/io/message_sender.cpp: -------------------------------------------------------------------------------- 1 | #include "io/message_sender.hpp" 2 | -------------------------------------------------------------------------------- /src/io/message_sender.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_IO_MESSAGE_SENDER_HPP_INCLUDED 2 | #define SHADOW_CAST_IO_MESSAGE_SENDER_HPP_INCLUDED 3 | 4 | #include "io/message_handler.hpp" 5 | 6 | namespace sc 7 | { 8 | template 9 | struct SendHandler 10 | { 11 | auto operator()(int fd, msghdr const& msg, T const&) noexcept -> ssize_t 12 | { 13 | return ::sendmsg(fd, &msg, 0); 14 | } 15 | }; 16 | 17 | template 18 | using MessageSender = MessageHandler, decltype(io::write)>; 19 | 20 | } // namespace sc 21 | 22 | #endif // SHADOW_CAST_IO_MESSAGE_SENDER_HPP_INCLUDED 23 | -------------------------------------------------------------------------------- /src/io/process.cpp: -------------------------------------------------------------------------------- 1 | #include "io/process.hpp" 2 | #include "utils/contracts.hpp" 3 | #include "utils/scope_guard.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace std::literals::string_literals; 14 | 15 | namespace sc 16 | { 17 | Process::Process(pid_t pid) noexcept 18 | : pid_ { pid } 19 | { 20 | } 21 | 22 | Process::Process(Process&& other) noexcept 23 | : pid_ { std::exchange(other.pid_, -1) } 24 | { 25 | } 26 | 27 | Process::~Process() { static_cast(terminate_and_wait()); } 28 | 29 | auto Process::operator=(Process&& other) noexcept -> Process& 30 | { 31 | using std::swap; 32 | 33 | auto tmp { std::move(other) }; 34 | swap(*this, tmp); 35 | return *this; 36 | } 37 | 38 | auto swap(Process& lhs, Process& rhs) noexcept -> void 39 | { 40 | using std::swap; 41 | swap(lhs.pid_, rhs.pid_); 42 | } 43 | 44 | auto Process::terminate() noexcept -> void 45 | { 46 | if (pid_ <= 0) 47 | return; 48 | 49 | ::kill(pid_, SIGINT); 50 | } 51 | 52 | auto Process::wait() noexcept -> int 53 | { 54 | SC_SCOPE_GUARD([&] { pid_ = -1; }); 55 | if (pid_ <= 0) 56 | return 0; 57 | 58 | int wstatus; 59 | ::waitpid(pid_, &wstatus, WEXITED); 60 | if (!WIFEXITED(wstatus)) 61 | return 1; 62 | 63 | return WEXITSTATUS(wstatus); 64 | } 65 | 66 | auto Process::terminate_and_wait() noexcept -> int 67 | { 68 | terminate(); 69 | return wait(); 70 | } 71 | 72 | auto spawn_process(std::span args) -> Process 73 | { 74 | /* TODO: 75 | */ 76 | std::vector child_args(args.size() + 1, nullptr); 77 | std::transform(args.begin(), 78 | args.end(), 79 | child_args.begin(), 80 | [](auto const& str) { return str.c_str(); }); 81 | 82 | auto pid = fork(); 83 | if (pid < 0) 84 | throw std::runtime_error { "spawn_process failed: "s + 85 | std::strerror(errno) }; 86 | 87 | if (pid == 0) { 88 | ::execvp(child_args.front(), 89 | const_cast(child_args.data())); 90 | } 91 | 92 | return Process { pid }; 93 | } 94 | 95 | } // namespace sc 96 | -------------------------------------------------------------------------------- /src/io/process.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_UTILS_PROCESS_HPP_INCLUDED 2 | #define SHADOW_CAST_UTILS_PROCESS_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace sc 9 | { 10 | 11 | struct Process 12 | { 13 | friend auto spawn_process(std::span /*args*/) -> Process; 14 | friend auto swap(Process&, Process&) noexcept -> void; 15 | 16 | Process() noexcept = default; 17 | Process(Process&&) noexcept; 18 | ~Process(); 19 | 20 | auto operator=(Process&&) noexcept -> Process&; 21 | 22 | auto terminate() noexcept -> void; 23 | [[nodiscard]] auto wait() noexcept -> int; 24 | [[nodiscard]] auto terminate_and_wait() noexcept -> int; 25 | 26 | private: 27 | Process(pid_t /*pid*/) noexcept; 28 | pid_t pid_ { -1 }; 29 | }; 30 | 31 | auto spawn_process(std::span /*args*/) -> Process; 32 | 33 | } // namespace sc 34 | 35 | #endif // SHADOW_CAST_UTILS_PROCESS_HPP_INCLUDED 36 | -------------------------------------------------------------------------------- /src/io/signals.cpp: -------------------------------------------------------------------------------- 1 | #include "io/signals.hpp" 2 | #include 3 | #include 4 | 5 | namespace sc 6 | { 7 | 8 | auto block_signals(std::initializer_list sigs) -> void 9 | { 10 | sigset_t blocked_signals; 11 | sigemptyset(&blocked_signals); 12 | for (auto sig : sigs) { 13 | if (auto const result = sigaddset(&blocked_signals, sig); result < 0) 14 | throw std::system_error { errno, std::system_category() }; 15 | } 16 | if (auto const result = 17 | pthread_sigmask(SIG_SETMASK, &blocked_signals, nullptr); 18 | result < 0) 19 | throw std::system_error { errno, std::system_category() }; 20 | } 21 | 22 | } // namespace sc 23 | -------------------------------------------------------------------------------- /src/io/signals.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_IO_SIGNALS_HPP_INCLUDED 2 | #define SHADOW_CAST_IO_SIGNALS_HPP_INCLUDED 3 | 4 | #include 5 | 6 | namespace sc 7 | { 8 | auto block_signals(std::initializer_list /*sigs*/) -> void; 9 | } // namespace sc 10 | #endif // SHADOW_CAST_IO_SIGNALS_HPP_INCLUDED 11 | -------------------------------------------------------------------------------- /src/io/unix_socket.cpp: -------------------------------------------------------------------------------- 1 | #include "io/unix_socket.hpp" 2 | #include "utils/scope_guard.hpp" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | using namespace std::literals::string_literals; 15 | 16 | namespace 17 | { 18 | 19 | template 20 | auto create_socket(std::string_view path, F&& on_create) 21 | { 22 | int fd = socket(AF_UNIX, SOCK_STREAM, 0); 23 | if (fd < 0) 24 | throw std::runtime_error { "connect(): "s + std::strerror(errno) }; 25 | 26 | auto close_guard = sc::ScopeGuard { [&] { close(fd); } }; 27 | 28 | sockaddr_un addr {}; 29 | addr.sun_family = AF_UNIX; 30 | if (path.size() > std::size(addr.sun_path)) 31 | throw std::runtime_error { "create_socket(): path too long" }; 32 | 33 | auto&& result = on_create(fd, addr); 34 | close_guard.deactivate(); 35 | return result; 36 | } 37 | 38 | } // namespace 39 | 40 | namespace sc 41 | { 42 | 43 | UnixSocket::UnixSocket(UnixSocket&& other) noexcept 44 | : fd_ { std::exchange(other.fd_, -1) } 45 | { 46 | } 47 | 48 | UnixSocket::UnixSocket(int fd) noexcept 49 | : fd_ { fd } 50 | { 51 | } 52 | 53 | UnixSocket::~UnixSocket() { close(); } 54 | 55 | auto UnixSocket::operator=(UnixSocket&& other) noexcept -> UnixSocket& 56 | { 57 | using std::swap; 58 | auto tmp { std::move(other) }; 59 | 60 | swap(*this, tmp); 61 | return *this; 62 | } 63 | 64 | auto UnixSocket::close() noexcept -> int { return ::close(fd_); } 65 | 66 | auto UnixSocket::accept() -> UnixSocket 67 | { 68 | sockaddr_un remote_addr; 69 | socklen_t len = sizeof(sockaddr_un); 70 | auto fd = ::accept(fd_, reinterpret_cast(&remote_addr), &len); 71 | if (fd < 0) 72 | throw std::runtime_error { "UnixSocket::accept() "s + 73 | std::strerror(errno) }; 74 | 75 | return UnixSocket { fd }; 76 | } 77 | 78 | auto UnixSocket::listen() -> void 79 | { 80 | if (auto const listen_result = ::listen(fd_, 1); listen_result < 0) 81 | throw std::runtime_error { "UnixSocket::listen() "s + 82 | std::strerror(errno) }; 83 | } 84 | 85 | auto UnixSocket::fd() const noexcept -> int { return fd_; } 86 | 87 | auto swap(UnixSocket& lhs, UnixSocket& rhs) noexcept -> void 88 | { 89 | using std::swap; 90 | swap(lhs.fd_, rhs.fd_); 91 | } 92 | 93 | namespace socket 94 | { 95 | 96 | auto bind(std::string_view path) -> UnixSocket 97 | { 98 | return create_socket(path, [&](int fd, sockaddr_un addr) { 99 | if (auto const result = bind(fd, 100 | reinterpret_cast(&addr), 101 | sizeof(addr.sun_family) + path.size()); 102 | result < 0) 103 | throw std::logic_error { "bind(): "s + std::strerror(errno) }; 104 | 105 | return UnixSocket { fd }; 106 | }); 107 | } 108 | 109 | auto connect(std::string_view path) -> UnixSocket 110 | { 111 | return create_socket(path, [&](int fd, sockaddr_un addr) { 112 | if (auto const result = connect(fd, 113 | reinterpret_cast(&addr), 114 | sizeof(addr.sun_family) + path.size()); 115 | result < 0) 116 | throw std::logic_error { "connect(): "s + std::strerror(errno) }; 117 | 118 | return UnixSocket { fd }; 119 | }); 120 | } 121 | 122 | } // namespace socket 123 | } // namespace sc 124 | -------------------------------------------------------------------------------- /src/io/unix_socket.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_IO_UNIX_SOCKET_HPP_INCLUDED 2 | #define SHADOW_CAST_IO_UNIX_SOCKET_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | 7 | namespace sc 8 | { 9 | struct UnixSocket; 10 | 11 | namespace socket 12 | { 13 | auto bind(std::string_view /*path*/) -> UnixSocket; 14 | auto connect(std::string_view /*path*/) -> UnixSocket; 15 | } // namespace socket 16 | 17 | struct UnixSocket 18 | { 19 | friend auto swap(UnixSocket&, UnixSocket&) noexcept -> void; 20 | friend auto socket::bind(std::string_view /*path*/) -> UnixSocket; 21 | friend auto socket::connect(std::string_view /*path*/) -> UnixSocket; 22 | 23 | UnixSocket() noexcept = default; 24 | UnixSocket(UnixSocket&&) noexcept; 25 | ~UnixSocket(); 26 | 27 | auto operator=(UnixSocket&&) noexcept -> UnixSocket&; 28 | 29 | auto close() noexcept -> int; 30 | auto accept() -> UnixSocket; 31 | auto listen() -> void; 32 | auto fd() const noexcept -> int; 33 | 34 | template 35 | auto use_with(F&& f) 36 | { 37 | return std::forward(f)(fd_); 38 | } 39 | 40 | UnixSocket(int /*fd*/) noexcept; 41 | 42 | int fd_ { -1 }; 43 | }; 44 | 45 | auto swap(UnixSocket&, UnixSocket&) noexcept -> void; 46 | 47 | } // namespace sc 48 | #endif // SHADOW_CAST_IO_UNIX_SOCKET_HPP_INCLUDED 49 | -------------------------------------------------------------------------------- /src/logging.cpp: -------------------------------------------------------------------------------- 1 | #include "./logging.hpp" 2 | #include 3 | #include 4 | 5 | namespace 6 | { 7 | 8 | auto level_to_string(sc::log::Level level) -> char const* 9 | { 10 | using sc::log::Level; 11 | 12 | switch (level) { 13 | case Level::info: 14 | return "INFO"; 15 | case Level::warning: 16 | return "WARN"; 17 | case Level::error: 18 | return "ERROR"; 19 | default: 20 | return "DEBUG"; 21 | } 22 | } 23 | 24 | auto format_level(sc::log::Level level) -> std::string 25 | { 26 | auto constexpr kMaxLevelStringSize = 5; 27 | std::string output(kMaxLevelStringSize + 3, ' '); 28 | output.front() = '['; 29 | *std::next(output.end(), -2) = ']'; 30 | 31 | std::string_view level_as_string = level_to_string(level); 32 | auto out_pos = std::next(output.begin()); 33 | 34 | std::copy_n(begin(level_as_string), kMaxLevelStringSize, out_pos); 35 | return output; 36 | } 37 | 38 | } // namespace 39 | 40 | namespace sc::log 41 | { 42 | 43 | auto write(Level level, std::string const& msg) -> void 44 | { 45 | write(level, std::string_view { msg.data(), msg.size() }); 46 | } 47 | 48 | auto write(Level level, std::string_view msg) -> void 49 | { 50 | auto const level_string = format_level(level); 51 | std::copy(begin(level_string), 52 | end(level_string), 53 | std::ostreambuf_iterator(std::cerr)); 54 | std::copy(begin(msg), end(msg), std::ostreambuf_iterator(std::cerr)); 55 | std::cerr.write("\n", 1); 56 | } 57 | 58 | } // namespace sc::log 59 | -------------------------------------------------------------------------------- /src/logging.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_LOGGING_HPP_INCLUDED 2 | #define SHADOW_CAST_LOGGING_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace sc::log 9 | { 10 | 11 | template 12 | concept StringLike = 13 | std::is_convertible_v, std::string> || 14 | std::is_convertible_v, std::string_view>; 15 | 16 | enum struct Level 17 | { 18 | debug, 19 | info, 20 | warning, 21 | error 22 | }; 23 | 24 | auto write(Level, std::string const&) -> void; 25 | auto write(Level, std::string_view) -> void; 26 | 27 | template 28 | auto debug(Msg&& msg) -> void 29 | { 30 | write(Level::debug, std::forward(msg)); 31 | } 32 | 33 | template 34 | auto info(Msg&& msg) -> void 35 | { 36 | write(Level::info, std::forward(msg)); 37 | } 38 | 39 | template 40 | auto warn(Msg&& msg) -> void 41 | { 42 | write(Level::warning, std::forward(msg)); 43 | } 44 | 45 | template 46 | auto error(Msg&& msg) -> void 47 | { 48 | write(Level::error, std::forward(msg)); 49 | } 50 | } // namespace sc::log 51 | 52 | #endif // SHADOW_CAST_LOGGING_HPP_INCLUDED 53 | -------------------------------------------------------------------------------- /src/metrics/formatting.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_METRICS_FORMATTING_HPP_INCLUDED 2 | #define SHADOW_CAST_METRICS_FORMATTING_HPP_INCLUDED 3 | 4 | #include "metrics/histogram.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace sc::metrics 13 | { 14 | 15 | template 16 | auto format_histogram_rows(std::basic_ostream& os, 17 | Histogram const& data, 18 | std::size_t column_size) -> void 19 | { 20 | std::for_each( 21 | std::begin(data), std::end(data), [&](auto const& data_point) { 22 | os << "| "; 23 | os.width(column_size); 24 | os << std::get<0>(data_point); 25 | os << " | "; 26 | 27 | os.width(column_size); 28 | os << std::get<1>(data_point) << " |\n"; 29 | }); 30 | } 31 | 32 | template 33 | auto format_histogram(std::basic_ostream& os, 34 | Histogram const& data, 35 | std::string_view series_name, 36 | std::string_view title = "") -> void 37 | { 38 | if (title.size()) { 39 | os << title << '\n'; 40 | } 41 | 42 | constexpr std::size_t kColumnMargin = 1; 43 | auto const column_width = std::max(series_name.size(), 10ul); 44 | 45 | os << "| "; 46 | os.width(column_width); 47 | os << series_name << " | "; 48 | os.width(column_width); 49 | 50 | os << "Frequency" 51 | << " |\n"; 52 | 53 | os.put('|').fill('-'); 54 | 55 | os.width(column_width + kColumnMargin * 2); 56 | os << '-' << '|'; 57 | os.width(column_width + kColumnMargin * 2); 58 | os << '-' << '|'; 59 | os.put('\n').fill(' '); 60 | 61 | format_histogram_rows(os, data, column_width); 62 | } 63 | 64 | template 65 | auto format_histogram(std::basic_ostream& os, 66 | Histogram const& data) -> void 67 | { 68 | format_histogram(os, data, "Buckets"); 69 | } 70 | } // namespace sc::metrics 71 | 72 | #endif // SHADOW_CAST_METRICS_FORMATTING_HPP_INCLUDED 73 | -------------------------------------------------------------------------------- /src/metrics/histogram.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_METRICS_HISTOGRAM_HPP_INCLUDED 2 | #define SHADOW_CAST_METRICS_HISTOGRAM_HPP_INCLUDED 3 | 4 | #include "utils/contracts.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace sc::metrics 15 | { 16 | 17 | // clang-format off 18 | template 19 | concept HistogramBucketConcept = 20 | std::is_default_constructible_v && 21 | requires(T val) { 22 | val += val; 23 | { val < val } -> std::convertible_to; 24 | { val + val } -> std::convertible_to; 25 | }; 26 | // clang-format on 27 | 28 | template 29 | requires(N >= 1) 30 | struct Histogram 31 | { 32 | friend struct HistogramIterator; 33 | using value_type = std::pair; 34 | 35 | struct HistogramIterator 36 | { 37 | using difference_type = std::ptrdiff_t; 38 | using value_type = typename Histogram::value_type; 39 | using category = std::output_iterator_tag; 40 | 41 | auto operator*() const noexcept -> value_type 42 | { 43 | return (*histogram)[n]; 44 | } 45 | 46 | auto operator++() noexcept -> HistogramIterator& 47 | { 48 | n += 1; 49 | return *this; 50 | } 51 | 52 | auto operator++(int) noexcept -> HistogramIterator 53 | { 54 | auto tmp { *this }; 55 | (*this).operator++(); 56 | return tmp; 57 | } 58 | 59 | auto operator==(HistogramIterator const& lhs) noexcept -> bool 60 | { 61 | return n == lhs.n; 62 | } 63 | 64 | auto operator!=(HistogramIterator const& lhs) noexcept -> bool 65 | { 66 | return n != lhs.n; 67 | } 68 | 69 | std::size_t n; 70 | Histogram const* histogram; 71 | }; 72 | 73 | Histogram() noexcept 74 | { 75 | T bucket_val = I; 76 | auto n = N; 77 | auto bucket_iterator = buckets_.begin(); 78 | while (n--) { 79 | *bucket_iterator++ = bucket_val; 80 | bucket_val += I; 81 | } 82 | } 83 | 84 | [[nodiscard]] auto operator[](std::size_t n) const noexcept -> value_type 85 | { 86 | SC_EXPECT(n < N); 87 | return value_type { buckets_[n], counts_[n] }; 88 | } 89 | 90 | auto add_value(T const& val) noexcept -> void 91 | { 92 | auto const n = std::distance( 93 | buckets_.begin(), 94 | std::lower_bound( 95 | buckets_.begin(), std::next(buckets_.begin(), N - 1), val)); 96 | 97 | counts_[n] += 1; 98 | } 99 | 100 | [[nodiscard]] auto begin() const noexcept -> HistogramIterator 101 | { 102 | return HistogramIterator { 0, this }; 103 | } 104 | 105 | [[nodiscard]] auto end() const noexcept -> HistogramIterator 106 | { 107 | return HistogramIterator { N, this }; 108 | } 109 | 110 | private: 111 | std::array buckets_; 112 | std::array counts_ {}; 113 | }; 114 | 115 | } // namespace sc::metrics 116 | #endif // SHADOW_CAST_METRICS_HISTOGRAM_HPP_INCLUDED 117 | -------------------------------------------------------------------------------- /src/metrics/metrics.cpp: -------------------------------------------------------------------------------- 1 | #include "metrics/metrics.hpp" 2 | 3 | namespace 4 | { 5 | 6 | auto audio_histogram() noexcept -> sc::metrics::AudioFrameTimeHistogram& 7 | { 8 | static sc::metrics::AudioFrameTimeHistogram histogram {}; 9 | return histogram; 10 | } 11 | 12 | auto video_histogram() noexcept -> sc::metrics::VideoFrameTimeHistogram& 13 | { 14 | static sc::metrics::VideoFrameTimeHistogram histogram {}; 15 | return histogram; 16 | } 17 | 18 | } // namespace 19 | 20 | namespace sc::metrics 21 | { 22 | auto add_frame_time(AudioMetricsTag, std::uint64_t value) noexcept -> void 23 | { 24 | audio_histogram().add_value(value); 25 | } 26 | 27 | auto add_frame_time(VideoMetricsTag, std::uint64_t value) noexcept -> void 28 | { 29 | video_histogram().add_value(value); 30 | } 31 | 32 | auto get_histogram(AudioMetricsTag) noexcept -> AudioFrameTimeHistogram const& 33 | { 34 | return audio_histogram(); 35 | } 36 | 37 | auto get_histogram(VideoMetricsTag) noexcept -> VideoFrameTimeHistogram const& 38 | { 39 | return video_histogram(); 40 | } 41 | 42 | } // namespace sc::metrics 43 | -------------------------------------------------------------------------------- /src/metrics/metrics.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_METRICS_METRICS_HPP_INCLUDED 2 | #define SHADOW_CAST_METRICS_METRICS_HPP_INCLUDED 3 | 4 | #include "./histogram.hpp" 5 | #include 6 | #include 7 | 8 | namespace sc::metrics 9 | { 10 | 11 | // clang-format off 12 | struct AudioMetricsTag { }; 13 | struct VideoMetricsTag { }; 14 | // clang-format on 15 | 16 | constexpr AudioMetricsTag audio_metrics {}; 17 | constexpr VideoMetricsTag video_metrics {}; 18 | 19 | constexpr std::uint64_t kBucketSize = 20 | std::chrono::duration_cast( 21 | std::chrono::milliseconds(1)) 22 | .count() * 23 | 0.5; 24 | 25 | constexpr std::size_t kBucketCount = 20; 26 | 27 | using AudioFrameTimeHistogram = 28 | Histogram; 29 | using VideoFrameTimeHistogram = 30 | Histogram; 31 | 32 | auto add_frame_time(AudioMetricsTag, std::uint64_t value) noexcept -> void; 33 | auto add_frame_time(VideoMetricsTag, std::uint64_t value) noexcept -> void; 34 | [[nodiscard]] auto get_histogram(AudioMetricsTag) noexcept 35 | -> AudioFrameTimeHistogram const&; 36 | [[nodiscard]] auto get_histogram(VideoMetricsTag) noexcept 37 | -> VideoFrameTimeHistogram const&; 38 | 39 | } // namespace sc::metrics 40 | 41 | #endif // SHADOW_CAST_METRICS_METRICS_HPP_INCLUDED 42 | -------------------------------------------------------------------------------- /src/nvidia.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_NVIDIA_HPP_INCLUDED 2 | #define SHADOW_CAST_NVIDIA_HPP_INCLUDED 3 | 4 | #include "./nvidia/NvFBC.h" 5 | #include "./nvidia/cuda.hpp" 6 | #include "./utils.hpp" 7 | #include "error.hpp" 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace sc 14 | { 15 | 16 | [[maybe_unused]] auto constexpr kNvFBCKeyEnvVar = "SHADOW_CAST_NVFBC_KEY"; 17 | 18 | using NvFBC = NVFBC_API_FUNCTION_LIST; 19 | struct NvFBCHandleDeleter 20 | { 21 | using pointer = NonPointer; 22 | NvFBCHandleDeleter(NvFBC fbc) noexcept; 23 | 24 | protected: 25 | NvFBC fbc_; 26 | }; 27 | 28 | struct NvFBCError : std::runtime_error 29 | { 30 | explicit NvFBCError(NvFBC, NVFBC_SESSION_HANDLE); 31 | }; 32 | 33 | struct NvFBCSessionHandleDeleter : NvFBCHandleDeleter 34 | { 35 | using NvFBCHandleDeleter::NvFBCHandleDeleter; 36 | auto operator()(pointer) noexcept -> void; 37 | }; 38 | 39 | struct NvFBCCaptureSessionHandleDeleter : NvFBCHandleDeleter 40 | { 41 | using NvFBCHandleDeleter::NvFBCHandleDeleter; 42 | auto operator()(pointer) noexcept -> void; 43 | }; 44 | 45 | using NvFBCSessionHandlePtr = 46 | std::unique_ptr; 47 | 48 | using NvFBCCaptureSessionHandlePtr = 49 | std::unique_ptr; 50 | 51 | struct NvFBCLib 52 | { 53 | friend auto load_nvfbc() -> NvFBCLib; 54 | auto NvFBCCreateInstance() -> NvFBC; 55 | 56 | private: 57 | PNVFBCCREATEINSTANCE NvFBCCreateInstance_; 58 | }; 59 | 60 | auto load_nvfbc() -> NvFBCLib; 61 | 62 | [[nodiscard]] auto create_nvfbc_session(sc::NvFBC instance) 63 | -> NvFBCSessionHandlePtr; 64 | 65 | auto create_nvfbc_capture_session(NVFBC_SESSION_HANDLE nvfbc_handle, 66 | NvFBC nvfbc, 67 | FrameTime const&) -> void; 68 | auto destroy_nvfbc_capture_session(NVFBC_SESSION_HANDLE nvfbc_handle, 69 | NvFBC nvfbc) -> void; 70 | } // namespace sc 71 | #endif // SHADOW_CAST_NVIDIA_HPP_INCLUDED 72 | -------------------------------------------------------------------------------- /src/platform.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_PLATFORM_HPP_INCLUDED 2 | #define SHADOW_CAST_PLATFORM_HPP_INCLUDED 3 | 4 | #include "./platform/egl.hpp" 5 | #include "./platform/wayland.hpp" 6 | 7 | #endif // SHADOW_CAST_PLATFORM_HPP_INCLUDED 8 | -------------------------------------------------------------------------------- /src/platform/egl.cpp: -------------------------------------------------------------------------------- 1 | #include "platform/egl.hpp" 2 | #include "error.hpp" 3 | #include "platform/opengl.hpp" 4 | #include "utils/symbol.hpp" 5 | 6 | #define TRY_ATTACH_EXTENSION_SYMBOL(target, name, egl) \ 7 | ::attach_extension_symbol(target, name, egl) 8 | 9 | namespace 10 | { 11 | template 12 | auto attach_extension_symbol(F** fn, char const* name, sc::EGL egl) -> void 13 | { 14 | *fn = reinterpret_cast(egl.eglGetProcAddress(name)); 15 | if (!*fn) 16 | throw sc::ModuleError { 17 | std::string { "Couldn't load extension symbol: " } + name 18 | }; 19 | } 20 | 21 | auto load_egl() -> sc::EGL 22 | { 23 | auto lib = dlopen("libEGL.so.1", RTLD_LAZY); 24 | if (!lib) 25 | throw sc::ModuleError { std::string { "Couldn't load libEGL.so.1: " } + 26 | dlerror() }; 27 | 28 | sc::EGL egl {}; 29 | TRY_ATTACH_SYMBOL(&egl.eglCreateImage, "eglCreateImage", lib); 30 | TRY_ATTACH_SYMBOL(&egl.eglDestroyImage, "eglDestroyImage", lib); 31 | TRY_ATTACH_SYMBOL(&egl.eglGetError, "eglGetError", lib); 32 | TRY_ATTACH_SYMBOL(&egl.eglSwapInterval, "eglSwapInterval", lib); 33 | TRY_ATTACH_SYMBOL(&egl.eglGetDisplay, "eglGetDisplay", lib); 34 | TRY_ATTACH_SYMBOL(&egl.eglGetPlatformDisplay, "eglGetPlatformDisplay", lib); 35 | TRY_ATTACH_SYMBOL(&egl.eglInitialize, "eglInitialize", lib); 36 | TRY_ATTACH_SYMBOL(&egl.eglTerminate, "eglTerminate", lib); 37 | TRY_ATTACH_SYMBOL(&egl.eglBindAPI, "eglBindAPI", lib); 38 | TRY_ATTACH_SYMBOL(&egl.eglChooseConfig, "eglChooseConfig", lib); 39 | TRY_ATTACH_SYMBOL(&egl.eglCreateContext, "eglCreateContext", lib); 40 | TRY_ATTACH_SYMBOL(&egl.eglDestroyContext, "eglDestroyContext", lib); 41 | TRY_ATTACH_SYMBOL( 42 | &egl.eglCreateWindowSurface, "eglCreateWindowSurface", lib); 43 | TRY_ATTACH_SYMBOL(&egl.eglDestroySurface, "eglDestroySurface", lib); 44 | TRY_ATTACH_SYMBOL(&egl.eglQueryString, "eglQueryString", lib); 45 | TRY_ATTACH_SYMBOL(&egl.eglGetProcAddress, "eglGetProcAddress", lib); 46 | TRY_ATTACH_SYMBOL(&egl.eglMakeCurrent, "eglMakeCurrent", lib); 47 | TRY_ATTACH_SYMBOL(&egl.eglSwapBuffers, "eglSwapBuffers", lib); 48 | 49 | return egl; 50 | } 51 | 52 | } // namespace 53 | 54 | namespace sc 55 | { 56 | 57 | auto egl() -> EGL const& 58 | { 59 | static EGL egl_module = load_egl(); 60 | return egl_module; 61 | } 62 | 63 | auto load_gl_extensions() -> void 64 | { 65 | auto& opengl = detail::get_opengl_module(); 66 | /* NOTE: 67 | * Extensions... 68 | */ 69 | TRY_ATTACH_EXTENSION_SYMBOL(&opengl.glEGLImageTargetTexture2DOES, 70 | "glEGLImageTargetTexture2DOES", 71 | egl()); 72 | } 73 | 74 | } // namespace sc 75 | -------------------------------------------------------------------------------- /src/platform/egl.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_EGL_HPP_INCLUDED 2 | #define SHADOW_CAST_EGL_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | 7 | using eglImageOES = void*; 8 | 9 | namespace sc 10 | { 11 | struct OpenGL; 12 | 13 | struct EGL 14 | { 15 | EGLImage (*eglCreateImage)(EGLDisplay dpy, 16 | EGLContext ctx, 17 | unsigned int target, 18 | EGLClientBuffer buffer, 19 | const intptr_t* attrib_list); 20 | 21 | unsigned int (*eglDestroyImage)(EGLDisplay dpy, EGLImage image); 22 | 23 | unsigned int (*eglGetError)(void); 24 | 25 | unsigned int (*eglSwapInterval)(EGLDisplay dpy, int32_t interval); 26 | 27 | EGLDisplay (*eglGetDisplay)(EGLNativeDisplayType display_id); 28 | EGLDisplay (*eglGetPlatformDisplay)(EGLenum platfor, 29 | void* native_display, 30 | EGLAttrib const* attrib_list); 31 | 32 | unsigned int (*eglInitialize)(EGLDisplay dpy, 33 | int32_t* major, 34 | int32_t* minor); 35 | 36 | unsigned int (*eglTerminate)(EGLDisplay dpy); 37 | 38 | unsigned int (*eglBindAPI)(unsigned int api); 39 | 40 | unsigned int (*eglChooseConfig)(EGLDisplay dpy, 41 | const int32_t* attrib_list, 42 | EGLConfig* configs, 43 | int32_t config_size, 44 | int32_t* num_config); 45 | 46 | EGLContext (*eglCreateContext)(EGLDisplay dpy, 47 | EGLConfig config, 48 | EGLContext share_context, 49 | const int32_t* attrib_list); 50 | 51 | unsigned int (*eglDestroyContext)(EGLDisplay dpy, EGLContext ctx); 52 | 53 | EGLSurface (*eglCreateWindowSurface)(EGLDisplay dpy, 54 | EGLConfig config, 55 | EGLNativeWindowType win, 56 | const int32_t* attrib_list); 57 | 58 | EGLSurface (*eglSwapBuffers)(EGLDisplay dpy, EGLSurface draw); 59 | unsigned int (*eglDestroySurface)(EGLDisplay dpy, EGLSurface ctx); 60 | 61 | char const* (*eglQueryString)(EGLDisplay dpy, EGLint name); 62 | void* (*eglGetProcAddress)(char const* name); 63 | unsigned int (*eglMakeCurrent)(EGLDisplay dpy, 64 | EGLSurface draw, 65 | EGLSurface read, 66 | EGLContext ctx); 67 | }; 68 | 69 | auto egl() -> EGL const&; 70 | 71 | auto load_gl_extensions() -> void; 72 | 73 | } // namespace sc 74 | 75 | #endif // SHADOW_CAST_EGL_HPP_INCLUDED 76 | -------------------------------------------------------------------------------- /src/platform/wayland.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_PLATFORM_WAYLAND_HPP_INCLUDED 2 | #define SHADOW_CAST_PLATFORM_WAYLAND_HPP_INCLUDED 3 | 4 | #include "./egl.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace sc 12 | { 13 | 14 | namespace wayland 15 | { 16 | 17 | struct DisplayDeleter 18 | { 19 | auto operator()(wl_display* ptr) const noexcept -> void; 20 | }; 21 | 22 | using DisplayPtr = std::unique_ptr; 23 | 24 | struct RegistryDeleter 25 | { 26 | auto operator()(wl_registry* ptr) const noexcept -> void; 27 | }; 28 | 29 | using RegistryPtr = std::unique_ptr; 30 | 31 | struct CompositorDeleter 32 | { 33 | auto operator()(wl_compositor* ptr) const noexcept -> void; 34 | }; 35 | 36 | using CompositorPtr = std::unique_ptr; 37 | 38 | struct SurfaceDeleter 39 | { 40 | auto operator()(wl_surface* ptr) const noexcept -> void; 41 | }; 42 | 43 | using SurfacePtr = std::unique_ptr; 44 | 45 | struct WindowDeleter 46 | { 47 | auto operator()(wl_egl_window* ptr) const noexcept -> void; 48 | }; 49 | 50 | using WindowPtr = std::unique_ptr; 51 | 52 | struct OutputDeleter 53 | { 54 | auto operator()(wl_output* ptr) const noexcept -> void; 55 | }; 56 | 57 | using OutputPtr = std::unique_ptr; 58 | 59 | } // namespace wayland 60 | 61 | struct EGLDeleter 62 | { 63 | EGLDeleter(EGL e, EGLDisplay d) noexcept; 64 | EGL egl; 65 | EGLDisplay display; 66 | }; 67 | 68 | struct EGLDisplayDeleter 69 | { 70 | auto operator()(EGLDisplay ptr) const noexcept -> void; 71 | EGL egl; 72 | }; 73 | 74 | using EGLDisplayPtr = 75 | std::unique_ptr, EGLDisplayDeleter>; 76 | 77 | struct EGLContextDeleter : EGLDeleter 78 | { 79 | using EGLDeleter::EGLDeleter; 80 | 81 | auto operator()(EGLContext ptr) const noexcept -> void; 82 | }; 83 | 84 | using EGLContextPtr = 85 | std::unique_ptr, EGLContextDeleter>; 86 | 87 | struct EGLSurfaceDeleter : EGLDeleter 88 | { 89 | using EGLDeleter::EGLDeleter; 90 | 91 | auto operator()(EGLSurface ptr) const noexcept -> void; 92 | }; 93 | 94 | using EGLSurfacePtr = 95 | std::unique_ptr, EGLSurfaceDeleter>; 96 | 97 | struct Wayland 98 | { 99 | wayland::DisplayPtr display; 100 | wayland::RegistryPtr registry; 101 | wayland::OutputPtr output; 102 | wayland::CompositorPtr compositor; 103 | wayland::SurfacePtr surface; 104 | wayland::WindowPtr window; 105 | 106 | std::uint32_t output_width { 0 }; 107 | std::uint32_t output_height { 0 }; 108 | }; 109 | 110 | struct WaylandEGL 111 | { 112 | EGLDisplayPtr egl_display; 113 | EGLSurfacePtr egl_surface; 114 | EGLContextPtr egl_context; 115 | }; 116 | 117 | auto initialize_wayland(wayland::DisplayPtr) -> std::unique_ptr; 118 | auto initialize_wayland_egl(EGL, Wayland const&) -> WaylandEGL; 119 | 120 | } // namespace sc 121 | 122 | #endif // SHADOW_CAST_PLATFORM_WAYLAND_HPP_INCLUDED 123 | -------------------------------------------------------------------------------- /src/services.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_SERVICES_HPP_INCLUDED 2 | #define SHADOW_CAST_SERVICES_HPP_INCLUDED 3 | 4 | #include "config.hpp" 5 | 6 | #include "./services/audio_service.hpp" 7 | #include "./services/context.hpp" 8 | #include "./services/drm_video_service.hpp" 9 | #include "./services/encoder.hpp" 10 | #include "./services/encoder_service.hpp" 11 | #include "./services/service.hpp" 12 | #include "./services/service_registry.hpp" 13 | #include "./services/signal_service.hpp" 14 | #include "./services/video_service.hpp" 15 | 16 | #endif // SHADOW_CAST_SERVICES_HPP_INCLUDED 17 | -------------------------------------------------------------------------------- /src/services/audio_service.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_AUDIO_SERVICE_HPP_INCLUDED 2 | #define SHADOW_CAST_AUDIO_SERVICE_HPP_INCLUDED 3 | 4 | #include "config.hpp" 5 | 6 | #include "av/media_chunk.hpp" 7 | #include "av/sample_format.hpp" 8 | #include "services/readiness.hpp" 9 | #include "services/service.hpp" 10 | #include "utils/intrusive_list.hpp" 11 | #include "utils/pool.hpp" 12 | #include "utils/receiver.hpp" 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace sc 21 | { 22 | 23 | struct AudioService; 24 | 25 | struct AudioLoopData 26 | { 27 | AudioService* service; 28 | pw_thread_loop* loop; 29 | pw_stream* stream; 30 | spa_audio_info format; 31 | SampleFormat required_sample_format; 32 | std::size_t required_sample_rate; 33 | }; 34 | 35 | auto on_process(void* userdata) -> void; 36 | 37 | struct AudioService final : Service 38 | { 39 | friend auto dispatch_chunks(Service&) -> void; 40 | friend auto add_chunk(AudioService&, SynchronizedPool::ItemPtr) 41 | -> void; 42 | 43 | using ChunkReceiverType = Receiver; 44 | using StreamEndReceiverType = Receiver; 45 | 46 | AudioService(SampleFormat, 47 | std::size_t /*sample_rate*/, 48 | std::size_t /*frame_size*/); 49 | 50 | ~AudioService(); 51 | /* AudioService must have a stable `this` pointer while 52 | * the h/w loop is running, so disable copy / move... 53 | */ 54 | AudioService(AudioService const&) = delete; 55 | auto operator=(AudioService const&) -> AudioService& = delete; 56 | 57 | friend auto on_process(void* userdata) -> void; 58 | 59 | protected: 60 | auto on_init(ReadinessRegister) -> void override; 61 | auto on_uninit() noexcept -> void override; 62 | 63 | public: 64 | template 65 | auto set_chunk_listener(F&& listener) -> void 66 | { 67 | chunk_listener_ = ChunkReceiverType { std::forward(listener) }; 68 | } 69 | 70 | template 71 | auto set_stream_end_listener(F&& listener) -> void 72 | { 73 | stream_end_listener_ = 74 | StreamEndReceiverType { std::forward(listener) }; 75 | } 76 | 77 | private: 78 | auto notify(std::size_t frames) noexcept -> void; 79 | 80 | std::optional chunk_listener_; 81 | std::optional stream_end_listener_; 82 | std::mutex data_mutex_; 83 | AudioLoopData loop_data_ {}; 84 | SampleFormat sample_format_; 85 | std::size_t sample_rate_; 86 | std::size_t frame_size_; 87 | MediaChunk input_buffer_; 88 | int event_fd_ { -1 }; 89 | }; 90 | 91 | auto dispatch_chunks(sc::Service&) -> void; 92 | 93 | } // namespace sc 94 | 95 | #endif // SHADOW_CAST_AUDIO_SERVICE_HPP_INCLUDED 96 | -------------------------------------------------------------------------------- /src/services/color_converter.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_SERVICES_COLOR_CONVERTER_HPP_INCLUDED 2 | #define SHADOW_CAST_SERVICES_COLOR_CONVERTER_HPP_INCLUDED 3 | 4 | #include "gl/buffer.hpp" 5 | #include "gl/framebuffer.hpp" 6 | #include "gl/program.hpp" 7 | #include "gl/texture.hpp" 8 | #include "gl/vertex_array_object.hpp" 9 | #include 10 | #include 11 | 12 | namespace sc 13 | { 14 | 15 | struct MouseParameters 16 | { 17 | std::uint32_t width, height; 18 | std::int32_t x, y; 19 | }; 20 | 21 | struct ColorConverter 22 | { 23 | ColorConverter(std::uint32_t output_width, 24 | std::uint32_t output_height) noexcept; 25 | 26 | auto initialize() -> void; 27 | [[nodiscard]] auto input_texture() noexcept -> opengl::Texture&; 28 | [[nodiscard]] auto mouse_texture() noexcept -> opengl::Texture&; 29 | [[nodiscard]] auto output_texture() noexcept -> opengl::Texture&; 30 | auto convert(std::optional mouse_params) -> void; 31 | 32 | private: 33 | opengl::Framebuffer fbo_; 34 | opengl::Texture input_texture_; 35 | opengl::Texture mouse_texture_; 36 | opengl::Texture output_texture_; 37 | opengl::VertexArray vao_; 38 | opengl::Buffer vertex_buffer_; 39 | opengl::Buffer texture_coords_buffer_; 40 | opengl::Buffer index_buffer_; 41 | opengl::Program program_; 42 | opengl::Program mouse_program_; 43 | std::uint32_t output_width_; 44 | std::uint32_t output_height_; 45 | GLuint mouse_dimensions_uniform_; 46 | GLuint mouse_position_uniform_; 47 | bool initialized_ { false }; 48 | }; 49 | 50 | } // namespace sc 51 | 52 | #endif // SHADOW_CAST_SERVICES_COLOR_CONVERTER_HPP_INCLUDED 53 | -------------------------------------------------------------------------------- /src/services/context.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_SERVICES_CONTEXT_HPP_INCLUDED 2 | #define SHADOW_CAST_SERVICES_CONTEXT_HPP_INCLUDED 3 | 4 | #include "services/service_registry.hpp" 5 | #include "utils/frame_time.hpp" 6 | #include 7 | 8 | namespace sc 9 | { 10 | 11 | struct Context 12 | { 13 | explicit Context(FrameTime const&) noexcept; 14 | explicit Context(std::uint32_t = 60) noexcept; 15 | Context(Context const&) = delete; 16 | auto operator=(Context const&) -> Context& = delete; 17 | 18 | auto run() -> void; 19 | auto services() noexcept -> ServiceRegistry&; 20 | auto request_stop() noexcept -> void; 21 | 22 | private: 23 | std::uint64_t frame_time_; 24 | std::atomic stop_requested_ { false }; 25 | ServiceRegistry reg_; 26 | int event_fd_ { -1 }; 27 | }; 28 | 29 | } // namespace sc 30 | 31 | #endif // SHADOW_CAST_SERVICES_CONTEXT_HPP_INCLUDED 32 | -------------------------------------------------------------------------------- /src/services/drm_video_service.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_SERVICES_DRM_VIDEO_SERVICE_HPP_INCLUDED 2 | #define SHADOW_CAST_SERVICES_DRM_VIDEO_SERVICE_HPP_INCLUDED 3 | 4 | #include "config.hpp" 5 | #include "services/color_converter.hpp" 6 | 7 | #include "drm/messaging.hpp" 8 | #include "io/process.hpp" 9 | #include "io/unix_socket.hpp" 10 | #include "nvidia.hpp" 11 | #include "platform/egl.hpp" 12 | #include "platform/wayland.hpp" 13 | #include "services/readiness.hpp" 14 | #include "services/service.hpp" 15 | #include "utils/borrowed_ptr.hpp" 16 | #include "utils/receiver.hpp" 17 | #include 18 | #include 19 | #include 20 | 21 | namespace sc 22 | { 23 | 24 | struct DRMVideoService final : Service 25 | { 26 | using CaptureFrameReceiverType = 27 | Receiver; 28 | 29 | explicit DRMVideoService(NvCuda nvcuda, 30 | CUcontext cuda_ctx, 31 | EGL& egl, 32 | Wayland& wayland, 33 | WaylandEGL& platform_egl) noexcept; 34 | 35 | template 36 | auto set_capture_frame_handler(F&& handler) -> void 37 | { 38 | frame_handler_ = CaptureFrameReceiverType { std::forward(handler) }; 39 | } 40 | 41 | protected: 42 | auto on_init(ReadinessRegister) -> void override; 43 | auto on_uninit() noexcept -> void override; 44 | 45 | private: 46 | static auto dispatch_frame(Service&) -> void; 47 | 48 | private: 49 | ColorConverter color_converter_; 50 | NvCuda nvcuda_; 51 | CUcontext cuda_ctx_; 52 | BorrowedPtr egl_; 53 | BorrowedPtr wayland_; 54 | BorrowedPtr platform_egl_; 55 | UnixSocket drm_socket_; 56 | Process drm_process_; 57 | sigset_t drm_proc_mask_; 58 | std::optional frame_handler_; 59 | CUgraphicsResource cuda_gfx_resource_ { nullptr }; 60 | CUarray cuda_array_ { nullptr }; 61 | std::uint64_t frame_time_ { 0 }; 62 | }; 63 | 64 | } // namespace sc 65 | 66 | #endif // SHADOW_CAST_SERVICES_DRM_VIDEO_SERVICE_HPP_INCLUDED 67 | -------------------------------------------------------------------------------- /src/services/encoder.cpp: -------------------------------------------------------------------------------- 1 | #include "services/encoder.hpp" 2 | #include "utils/contracts.hpp" 3 | #include "utils/pool.hpp" 4 | #include 5 | #include 6 | 7 | namespace sc 8 | { 9 | Encoder::Encoder(Context& ctx) noexcept 10 | : svc_ { ctx.services().use_if() } 11 | { 12 | SC_EXPECT(svc_); 13 | } 14 | 15 | auto Encoder::write_frame(Frame&& item) -> void 16 | { 17 | svc_->write_frame(std::move(item)); 18 | } 19 | 20 | auto Encoder::flush(BorrowedPtr codec_ctx, 21 | BorrowedPtr stream) -> void 22 | { 23 | auto encoder_frame = prepare_frame(codec_ctx, stream); 24 | encoder_frame->frame.reset(); 25 | svc_->write_frame(std::move(encoder_frame)); 26 | } 27 | 28 | auto Encoder::prepare_frame(BorrowedPtr codec_ctx, 29 | BorrowedPtr stream) -> Frame 30 | { 31 | auto pool_item = svc_->pool().get(); 32 | pool_item->codec_ctx = codec_ctx.get(); 33 | pool_item->stream = stream.get(); 34 | return pool_item; 35 | } 36 | 37 | } // namespace sc 38 | -------------------------------------------------------------------------------- /src/services/encoder.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_SERVICES_MEDIA_WRITER_HPP_INCLUDED 2 | #define SHADOW_CAST_SERVICES_MEDIA_WRITER_HPP_INCLUDED 3 | 4 | #include "services/context.hpp" 5 | #include "services/encoder_service.hpp" 6 | #include "utils/borrowed_ptr.hpp" 7 | #include "utils/pool.hpp" 8 | 9 | namespace sc 10 | { 11 | struct Encoder 12 | { 13 | using Frame = typename EncoderService::PoolType::ItemPtr; 14 | explicit Encoder(Context&) noexcept; 15 | 16 | auto write_frame(Frame&&) -> void; 17 | auto flush(BorrowedPtr, BorrowedPtr) -> void; 18 | 19 | auto prepare_frame(BorrowedPtr, BorrowedPtr) 20 | -> Frame; 21 | 22 | private: 23 | sc::BorrowedPtr svc_; 24 | }; 25 | } // namespace sc 26 | 27 | #endif // SHADOW_CAST_SERVICES_MEDIA_WRITER_HPP_INCLUDED 28 | -------------------------------------------------------------------------------- /src/services/encoder_service.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_SERVICES_MEDIA_WRITER_SERVICE_HPP_INCLUDED 2 | #define SHADOW_CAST_SERVICES_MEDIA_WRITER_SERVICE_HPP_INCLUDED 3 | 4 | #include "config.hpp" 5 | 6 | #include "av/frame.hpp" 7 | #include "av/packet.hpp" 8 | #include "services/readiness.hpp" 9 | #include "services/service.hpp" 10 | #include "utils/borrowed_ptr.hpp" 11 | #include "utils/intrusive_list.hpp" 12 | #include "utils/pool.hpp" 13 | extern "C" { 14 | #include 15 | #include 16 | } 17 | #include 18 | 19 | namespace sc 20 | { 21 | 22 | struct StreamPoll : ListItemBase 23 | { 24 | StreamPoll(); 25 | 26 | AVCodecContext* codec_ctx; 27 | AVStream* stream; 28 | FramePtr frame; 29 | 30 | auto reset() noexcept -> void; 31 | }; 32 | 33 | struct EncoderService final : Service 34 | { 35 | private: 36 | struct EncoderPoolLifetime 37 | { 38 | auto construct(StreamPoll* item) -> void 39 | { 40 | new (static_cast(item)) StreamPoll {}; 41 | item->frame = FramePtr { av_frame_alloc() }; 42 | } 43 | 44 | auto destruct(StreamPoll* item) -> void { item->~StreamPoll(); } 45 | }; 46 | 47 | public: 48 | using PoolType = SynchronizedPool; 49 | EncoderService(BorrowedPtr) noexcept; 50 | 51 | EncoderService(EncoderService const&) = delete; 52 | auto operator=(EncoderService const&) -> EncoderService& = delete; 53 | 54 | auto write_frame(PoolType::ItemPtr) -> void; 55 | 56 | auto pool() noexcept -> PoolType&; 57 | 58 | protected: 59 | auto on_init(ReadinessRegister) -> void override; 60 | auto on_uninit() noexcept -> void override; 61 | 62 | private: 63 | static auto dispatch(Service&) -> void; 64 | 65 | BorrowedPtr format_context_; 66 | int notify_fd_ { -1 }; 67 | std::mutex data_mutex_; 68 | PacketPtr packet_; 69 | PoolType pool_; 70 | IntrusiveList pending_; 71 | }; 72 | } // namespace sc 73 | 74 | #endif // SHADOW_CAST_SERVICES_MEDIA_WRITER_SERVICE_HPP_INCLUDED 75 | -------------------------------------------------------------------------------- /src/services/readiness.cpp: -------------------------------------------------------------------------------- 1 | #include "services/readiness.hpp" 2 | #include "services/service.hpp" 3 | #include 4 | 5 | namespace sc 6 | { 7 | 8 | FrameTimeRatio::FrameTimeRatio(std::size_t n, std::size_t d) noexcept 9 | : num { n } 10 | , denom { d } 11 | { 12 | } 13 | 14 | ReadinessRegister::ReadinessRegister(Service& svc, 15 | NotifyRegisterType& reg, 16 | FrameTickRegisterType& freg, 17 | std::size_t ftime) noexcept 18 | : current_svc_ { &svc } 19 | , notify_reg_ { ® } 20 | , frame_tick_reg_ { &freg } 21 | , frame_time_ { ftime } 22 | { 23 | } 24 | 25 | auto ReadinessRegister::operator()(RegisterType val, ServiceDispatch dispatch) 26 | -> void 27 | { 28 | std::visit( 29 | [&](auto&& arg) { 30 | using T = std::decay_t; 31 | if constexpr (std::is_same_v) { 32 | notify_reg_->emplace(arg, Readiness { current_svc_, dispatch }); 33 | } 34 | else { 35 | auto expiry = static_cast( 36 | static_cast(frame_time_) * 37 | (static_cast(arg.num) / arg.denom)); 38 | 39 | auto insert_pos = 40 | std::lower_bound(frame_tick_reg_->begin(), 41 | frame_tick_reg_->end(), 42 | expiry, 43 | [&](auto const& a, auto const& b) { 44 | return a.frame_time < b; 45 | }); 46 | 47 | frame_tick_reg_->emplace( 48 | insert_pos, 49 | FrameTickReadiness { 50 | { current_svc_, dispatch }, expiry, 0 }); 51 | } 52 | }, 53 | val); 54 | } 55 | 56 | auto ReadinessRegister::frame_time() const noexcept -> std::size_t 57 | { 58 | return frame_time_; 59 | } 60 | } // namespace sc 61 | -------------------------------------------------------------------------------- /src/services/readiness.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_READINESS_HPP_INCLUDED 2 | #define SHADOW_CAST_READINESS_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace sc 12 | { 13 | 14 | struct Service; 15 | 16 | using ServiceDispatch = auto(*)(Service&) -> void; 17 | 18 | struct Readiness 19 | { 20 | Service* svc; 21 | ServiceDispatch dispatch; 22 | }; 23 | 24 | struct FrameTickReadiness : Readiness 25 | { 26 | std::size_t frame_time; 27 | std::size_t remaining; 28 | }; 29 | 30 | struct FrameTimeRatio 31 | { 32 | explicit FrameTimeRatio(std::size_t n, std::size_t d = 1) noexcept; 33 | 34 | std::size_t num; 35 | std::size_t denom = 1; 36 | }; 37 | 38 | using RegisterType = std::variant; 39 | 40 | struct ReadinessRegister 41 | { 42 | using NotifyRegisterType = std::unordered_map; 43 | using FrameTickRegisterType = std::vector; 44 | 45 | explicit ReadinessRegister(Service&, 46 | NotifyRegisterType&, 47 | FrameTickRegisterType&, 48 | std::size_t) noexcept; 49 | 50 | auto operator()(RegisterType val, ServiceDispatch dispatch) -> void; 51 | 52 | auto frame_time() const noexcept -> std::size_t; 53 | 54 | private: 55 | Service* current_svc_; 56 | NotifyRegisterType* notify_reg_; 57 | FrameTickRegisterType* frame_tick_reg_; 58 | std::size_t frame_time_; 59 | }; 60 | 61 | } // namespace sc 62 | 63 | #endif // SHADOW_CAST_READINESS_HPP_INCLUDED 64 | -------------------------------------------------------------------------------- /src/services/service.cpp: -------------------------------------------------------------------------------- 1 | #include "services/service.hpp" 2 | 3 | namespace sc 4 | { 5 | 6 | Service::~Service() {} 7 | 8 | auto Service::init(ReadinessRegister register_readiness) -> void 9 | { 10 | on_init(register_readiness); 11 | } 12 | 13 | auto Service::uninit() noexcept -> void { on_uninit(); } 14 | 15 | auto Service::on_uninit() noexcept -> void {} 16 | 17 | } // namespace sc 18 | -------------------------------------------------------------------------------- /src/services/service.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_SERVICE_HPP_INCLUDED 2 | #define SHADOW_CAST_SERVICE_HPP_INCLUDED 3 | 4 | #include "services/readiness.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace sc 11 | { 12 | 13 | struct Service; 14 | 15 | struct Service 16 | { 17 | virtual ~Service(); 18 | auto init(ReadinessRegister) -> void; 19 | auto uninit() noexcept -> void; 20 | 21 | protected: 22 | virtual auto on_init(ReadinessRegister) -> void = 0; 23 | virtual auto on_uninit() noexcept -> void; 24 | }; 25 | 26 | } // namespace sc 27 | 28 | #endif // SHADOW_CAST_SERVICE_HPP_INCLUDED 29 | -------------------------------------------------------------------------------- /src/services/service_registry.cpp: -------------------------------------------------------------------------------- 1 | #include "./service_registry.hpp" 2 | 3 | namespace sc 4 | { 5 | namespace detail 6 | { 7 | auto service_id_impl() noexcept -> std::size_t 8 | { 9 | static std::size_t id = 1; 10 | return id++; 11 | } 12 | } // namespace detail 13 | // 14 | auto ServiceRegistry::init_all(ReadinessRegister reg) -> void 15 | { 16 | for (auto& [id, svc] : map_) { 17 | svc->init(reg); 18 | } 19 | } 20 | 21 | auto ServiceRegistry::uninit_all() -> void 22 | { 23 | for (auto& [id, svc] : map_) { 24 | svc->uninit(); 25 | } 26 | } 27 | 28 | auto ServiceRegistry::begin() noexcept -> decltype(map_.begin()) 29 | { 30 | return map_.begin(); 31 | } 32 | 33 | auto ServiceRegistry::end() noexcept -> decltype(map_.end()) 34 | { 35 | return map_.end(); 36 | } 37 | 38 | auto ServiceRegistry::begin() const noexcept -> decltype(map_.begin()) 39 | { 40 | return map_.begin(); 41 | } 42 | 43 | auto ServiceRegistry::end() const noexcept -> decltype(map_.end()) 44 | { 45 | return map_.end(); 46 | } 47 | 48 | auto ServiceRegistry::lock() noexcept -> bool { return lock_.test_and_set(); } 49 | 50 | auto ServiceRegistry::unlock() noexcept -> void { return lock_.clear(); } 51 | 52 | ServiceRegistryLock::ServiceRegistryLock(ServiceRegistry& reg) noexcept 53 | : reg_ { reg } 54 | , obtained_lock_ { !reg_.lock() } 55 | { 56 | } 57 | 58 | ServiceRegistryLock::~ServiceRegistryLock() 59 | { 60 | if (obtained_lock_) { 61 | reg_.unlock(); 62 | } 63 | } 64 | 65 | ServiceRegistryLock::operator bool() const noexcept { return obtained_lock_; } 66 | } // namespace sc 67 | -------------------------------------------------------------------------------- /src/services/service_registry.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_SERVICE_REGISTRY_HPP_INCLUDED 2 | #define SHADOW_CAST_SERVICE_REGISTRY_HPP_INCLUDED 3 | 4 | #include "./service.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace sc 11 | { 12 | 13 | namespace detail 14 | { 15 | auto service_id_impl() noexcept -> std::size_t; 16 | 17 | template 18 | struct ServiceIdProxy 19 | { 20 | static decltype(service_id_impl()) value; 21 | }; 22 | 23 | template 24 | decltype(service_id_impl()) ServiceIdProxy::value = service_id_impl(); 25 | 26 | } // namespace detail 27 | 28 | template 29 | auto service_id() noexcept -> decltype(detail::service_id_impl()) 30 | requires(std::is_convertible_v) 31 | { 32 | static auto id = detail::ServiceIdProxy::value; 33 | return id; 34 | } 35 | 36 | using ServicePtr = std::unique_ptr; 37 | using ServiceId = decltype(detail::service_id_impl()); 38 | 39 | template 40 | concept ServiceLike = 41 | std::is_convertible_v*, Service const*>; 42 | 43 | template 44 | concept ServiceFactory = 45 | std::is_constructible_v, decltype(std::declval()())>; 46 | 47 | struct ServiceRegistry; 48 | 49 | struct ServiceRegistryLock 50 | { 51 | explicit ServiceRegistryLock(ServiceRegistry&) noexcept; 52 | ~ServiceRegistryLock(); 53 | operator bool() const noexcept; 54 | 55 | private: 56 | ServiceRegistry& reg_; 57 | bool obtained_lock_; 58 | }; 59 | 60 | struct ServiceRegistry 61 | { 62 | friend struct ServiceRegistryLock; 63 | 64 | template F> 65 | auto add_from_factory(F&& f) 66 | { 67 | ServiceRegistryLock lock { *this }; 68 | if (!lock) [[unlikely]] { 69 | assert(false && "Couldn't obtain service registry lock"); 70 | } 71 | auto id = service_id(); 72 | map_.emplace(id, f()); 73 | } 74 | 75 | template 76 | auto add(T&& svc) -> void 77 | { 78 | ServiceRegistryLock lock { *this }; 79 | if (!lock) [[unlikely]] { 80 | assert(false && "Couldn't obtain service registry lock"); 81 | } 82 | auto id = service_id(); 83 | map_.emplace(id, std::make_unique(std::move(svc))); 84 | } 85 | 86 | // clang-format off 87 | template 88 | auto use() -> T& 89 | requires (std::is_default_constructible_v) 90 | { 91 | if (auto svc_pos = map_.find(service_id()); svc_pos != map_.end()) 92 | return static_cast(*svc_pos->second.get()); 93 | 94 | add(T {}); 95 | return use(); 96 | } 97 | // clang-format on 98 | 99 | // clang-format off 100 | template 101 | auto use_if() -> T* 102 | { 103 | if (auto svc_pos = map_.find(service_id()); svc_pos != map_.end()) 104 | return static_cast(svc_pos->second.get()); 105 | 106 | return nullptr; 107 | } 108 | // clang-format on 109 | 110 | auto init_all(ReadinessRegister reg) -> void; 111 | auto uninit_all() -> void; 112 | 113 | private: 114 | std::unordered_map map_; 115 | std::atomic_flag lock_ { false }; 116 | 117 | private: 118 | auto lock() noexcept -> bool; 119 | auto unlock() noexcept -> void; 120 | 121 | public: 122 | auto begin() noexcept -> decltype(map_.begin()); 123 | auto end() noexcept -> decltype(map_.end()); 124 | auto begin() const noexcept -> decltype(map_.begin()); 125 | auto end() const noexcept -> decltype(map_.end()); 126 | }; 127 | 128 | } // namespace sc 129 | 130 | #endif // SHADOW_CAST_SERVICE_REGISTRY_HPP_INCLUDED 131 | -------------------------------------------------------------------------------- /src/services/signal_service.cpp: -------------------------------------------------------------------------------- 1 | #include "services/signal_service.hpp" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace sc 10 | { 11 | 12 | SignalService::SignalService() 13 | : notify_fd_ { [] { 14 | sigset_t empty; 15 | sigemptyset(&empty); 16 | return signalfd(-1, &empty, SFD_NONBLOCK); 17 | }() } 18 | { 19 | if (notify_fd_ < 0) 20 | throw std::system_error { errno, std::system_category() }; 21 | 22 | if (auto const result = 23 | pthread_sigmask(SIG_SETMASK, nullptr, &original_mask_); 24 | result < 0) 25 | throw std::system_error { errno, std::system_category() }; 26 | } 27 | 28 | SignalService::SignalService(SignalService&& other) noexcept 29 | : original_mask_ { other.original_mask_ } 30 | , notify_fd_ { std::exchange(other.notify_fd_, -1) } 31 | , handlers_ { std::move(other.handlers_) } 32 | { 33 | } 34 | 35 | SignalService::~SignalService() { close(notify_fd_); } 36 | 37 | auto SignalService::on_init(ReadinessRegister reg) -> void 38 | { 39 | reg(notify_fd_, &dispatch_signal); 40 | } 41 | 42 | auto SignalService::on_uninit() noexcept -> void 43 | { 44 | pthread_sigmask(SIG_SETMASK, &original_mask_, nullptr); 45 | } 46 | 47 | auto dispatch_signal(Service& svc) -> void 48 | { 49 | auto& self = static_cast(svc); 50 | 51 | std::array buffer {}; 52 | auto const bytes = buffer.size() * sizeof(buffer[0]); 53 | 54 | while (true) { 55 | auto const read_result = read(self.notify_fd_, buffer.data(), bytes); 56 | if (read_result < 0) { 57 | if (errno == EAGAIN) { 58 | break; 59 | } 60 | throw std::system_error { errno, std::system_category() }; 61 | } 62 | 63 | auto const items = read_result / sizeof(buffer[0]); 64 | 65 | for (auto i = 0ul; i < items; ++i) { 66 | auto signo = buffer[i].ssi_signo; 67 | auto handler_pos = self.handlers_.find(signo); 68 | if (handler_pos == self.handlers_.end()) 69 | continue; 70 | 71 | (handler_pos->second)(signo); 72 | } 73 | 74 | if (items < buffer.size()) 75 | break; 76 | } 77 | } 78 | 79 | namespace detail 80 | { 81 | auto block_signal(std::uint32_t sig, int notify_fd) -> void 82 | { 83 | sigset_t working_set; 84 | if (auto const result = pthread_sigmask(SIG_SETMASK, nullptr, &working_set); 85 | result < 0) 86 | throw std::system_error { errno, std::system_category() }; 87 | 88 | if (auto const result = sigaddset(&working_set, sig); result < 0) 89 | throw std::system_error { errno, std::system_category() }; 90 | 91 | if (auto const result = pthread_sigmask(SIG_SETMASK, &working_set, nullptr); 92 | result < 0) 93 | throw std::system_error { errno, std::system_category() }; 94 | 95 | if (auto const result = signalfd(notify_fd, &working_set, SFD_NONBLOCK); 96 | result < 0) 97 | throw std::system_error { errno, std::system_category() }; 98 | } 99 | } // namespace detail 100 | 101 | } // namespace sc 102 | -------------------------------------------------------------------------------- /src/services/signal_service.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_SERVICES_SIGNAL_SERVICE_HPP_INCLUDED 2 | #define SHADOW_CAST_SERVICES_SIGNAL_SERVICE_HPP_INCLUDED 3 | 4 | #include "services/service.hpp" 5 | #include "utils/receiver.hpp" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace sc 14 | { 15 | 16 | namespace detail 17 | { 18 | auto block_signal(std::uint32_t, int) -> void; 19 | } 20 | 21 | struct SignalService final : Service 22 | { 23 | friend auto dispatch_signal(Service&) -> void; 24 | 25 | using SignalHandler = Receiver; 26 | 27 | SignalService(); 28 | ~SignalService(); 29 | 30 | SignalService(SignalService&&) noexcept; 31 | auto operator=(SignalService&&) noexcept = delete; 32 | 33 | SignalService(SignalService const&) = delete; 34 | auto operator=(SignalService const&) -> SignalService& = delete; 35 | 36 | template 37 | auto add_signal_handler(std::uint32_t sig, T&& handler) -> void 38 | { 39 | detail::block_signal(sig, notify_fd_); 40 | handlers_.emplace(sig, SignalHandler { std::forward(handler) }); 41 | } 42 | 43 | protected: 44 | auto on_init(ReadinessRegister) -> void override; 45 | auto on_uninit() noexcept -> void override; 46 | 47 | private: 48 | sigset_t original_mask_; 49 | int notify_fd_; 50 | std::unordered_map handlers_; 51 | }; 52 | 53 | auto dispatch_signal(Service&) -> void; 54 | } // namespace sc 55 | 56 | #endif // SHADOW_CAST_SERVICES_SIGNAL_SERVICE_HPP_INCLUDEDq 57 | -------------------------------------------------------------------------------- /src/services/video_service.cpp: -------------------------------------------------------------------------------- 1 | #include "services/video_service.hpp" 2 | #include "nvidia/NvFBC.h" 3 | #include "utils/contracts.hpp" 4 | 5 | #ifdef SHADOW_CAST_ENABLE_HISTOGRAMS 6 | #include "metrics/metrics.hpp" 7 | #endif 8 | 9 | namespace sc 10 | { 11 | 12 | VideoService::VideoService( 13 | NvFBC nvfbc, 14 | BorrowedPtr> nvcuda_ctx, 15 | NVFBC_SESSION_HANDLE nvfbc_session) noexcept 16 | : nvfbc_ { nvfbc } 17 | , nvcuda_ctx_ { nvcuda_ctx } 18 | , nvfbc_session_ { nvfbc_session } 19 | { 20 | } 21 | 22 | auto VideoService::on_init(ReadinessRegister reg) -> void 23 | { 24 | frame_time_ = reg.frame_time(); 25 | reg(FrameTimeRatio(1), &dispatch_frame); 26 | } 27 | 28 | auto dispatch_frame(Service& svc) -> void 29 | { 30 | auto& self = static_cast(svc); 31 | 32 | #ifdef SHADOW_CAST_ENABLE_HISTOGRAMS 33 | auto const frame_start = global_elapsed.nanosecond_value(); 34 | #endif 35 | 36 | CUdeviceptr cu_device_ptr {}; 37 | 38 | NVFBC_FRAME_GRAB_INFO frame_info {}; 39 | 40 | NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab_params {}; 41 | grab_params.dwVersion = NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER; 42 | grab_params.dwFlags = NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT; 43 | grab_params.pFrameGrabInfo = &frame_info; 44 | grab_params.pCUDADeviceBuffer = &cu_device_ptr; 45 | 46 | if (auto const status = 47 | self.nvfbc_.nvFBCToCudaGrabFrame(self.nvfbc_session_, &grab_params); 48 | status != NVFBC_SUCCESS) 49 | throw NvFBCError { self.nvfbc_, self.nvfbc_session_ }; 50 | 51 | if (self.receiver_) 52 | (*self.receiver_)(cu_device_ptr, frame_info, self.frame_time_); 53 | 54 | #ifdef SHADOW_CAST_ENABLE_HISTOGRAMS 55 | metrics::add_frame_time(metrics::video_metrics, 56 | global_elapsed.nanosecond_value() - frame_start); 57 | #endif 58 | } 59 | 60 | } // namespace sc 61 | -------------------------------------------------------------------------------- /src/services/video_service.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_VIDEO_SERVICE_HPP_INCLUDED 2 | #define SHADOW_CAST_VIDEO_SERVICE_HPP_INCLUDED 3 | 4 | #include "config.hpp" 5 | 6 | #include "nvidia.hpp" 7 | #include "services/service.hpp" 8 | #include "utils/borrowed_ptr.hpp" 9 | #include "utils/receiver.hpp" 10 | #include 11 | 12 | namespace sc 13 | { 14 | struct VideoService final : Service 15 | { 16 | friend auto dispatch_frame(Service&) -> void; 17 | 18 | using CaptureFrameReceiverType = 19 | Receiver; 20 | 21 | VideoService(NvFBC, 22 | BorrowedPtr>, 23 | NVFBC_SESSION_HANDLE) noexcept; 24 | 25 | template 26 | auto set_capture_frame_handler(F&& handler) -> void 27 | { 28 | receiver_ = CaptureFrameReceiverType { std::forward(handler) }; 29 | } 30 | 31 | protected: 32 | auto on_init(ReadinessRegister) -> void override; 33 | 34 | private: 35 | NvFBC nvfbc_; 36 | BorrowedPtr> nvcuda_ctx_; 37 | NVFBC_SESSION_HANDLE nvfbc_session_; 38 | 39 | std::optional receiver_; 40 | std::uint64_t frame_time_ { 0 }; 41 | }; 42 | 43 | auto dispatch_frame(Service&) -> void; 44 | 45 | } // namespace sc 46 | #endif // SHADOW_CAST_VIDEO_SERVICE_HPP_INCLUDED 47 | -------------------------------------------------------------------------------- /src/shadow_cast.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_HPP_INCLUDED 2 | #define SHADOW_CAST_HPP_INCLUDED 3 | 4 | #include "config.hpp" 5 | 6 | #include "./av.hpp" 7 | #include "./display.hpp" 8 | #include "./error.hpp" 9 | #include "./handlers.hpp" 10 | #include "./io.hpp" 11 | #include "./nvidia.hpp" 12 | #include "./platform.hpp" 13 | #include "./services.hpp" 14 | #include "./utils.hpp" 15 | 16 | #endif // SHADOW_CAST_HPP_INCLUDED 17 | -------------------------------------------------------------------------------- /src/utils.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_UTILS_HPP_INCLUDED 2 | #define SHADOW_CAST_UTILS_HPP_INCLUDED 3 | 4 | #include "./utils/base64.hpp" 5 | #include "./utils/borrowed_ptr.hpp" 6 | #include "./utils/cmd_line.hpp" 7 | #include "./utils/contracts.hpp" 8 | #include "./utils/elapsed.hpp" 9 | #include "./utils/frame_time.hpp" 10 | #include "./utils/intrusive_list.hpp" 11 | #include "./utils/non_pointer.hpp" 12 | #include "./utils/pool.hpp" 13 | #include "./utils/receiver.hpp" 14 | #include "./utils/result.hpp" 15 | #include "./utils/scope_guard.hpp" 16 | #include "./utils/symbol.hpp" 17 | 18 | #endif // SHADOW_CAST_UTILS_HPP_INCLUDED 19 | -------------------------------------------------------------------------------- /src/utils/base64.cpp: -------------------------------------------------------------------------------- 1 | #include "./base64.hpp" 2 | #include "config.hpp" 3 | 4 | namespace 5 | { 6 | 7 | template 8 | auto begin_by_byte_order(std::span s) noexcept 9 | requires(sc::kByteOrder == sc::ByteOrder::little_endian) 10 | { 11 | return s.rbegin(); 12 | } 13 | 14 | template 15 | auto begin_by_byte_order(std::span s) noexcept 16 | requires(sc::kByteOrder == sc::ByteOrder::big_endian) 17 | { 18 | return s.begin(); 19 | } 20 | 21 | auto constexpr base64_alphabet() noexcept -> std::array 22 | { 23 | std::array alphabet; 24 | auto pos = alphabet.begin(); 25 | 26 | for (auto c = 'A'; c <= 'Z'; ++c) 27 | *pos++ = c; 28 | for (auto c = 'a'; c <= 'z'; ++c) 29 | *pos++ = c; 30 | for (auto c = '0'; c <= '9'; ++c) 31 | *pos++ = c; 32 | *pos++ = '+'; 33 | *pos++ = '/'; 34 | 35 | return alphabet; 36 | } 37 | 38 | auto constexpr kBase64Alphabet = base64_alphabet(); 39 | 40 | auto base64_decode_chunk(std::string_view input) 41 | -> sc::Result 42 | { 43 | using std::distance; 44 | 45 | std::uint32_t val {}; 46 | int shift = 3; 47 | int n = 0; 48 | for (auto const& c : input) { 49 | if (c == '=') 50 | break; 51 | 52 | auto const pos = 53 | std::find(begin(kBase64Alphabet), end(kBase64Alphabet), c); 54 | if (pos == end(kBase64Alphabet)) 55 | return sc::Base64DecodeErrorCode::invalid_token; 56 | 57 | auto const i = distance(begin(kBase64Alphabet), pos); 58 | val |= (i & 0x3f) << ((6 * shift--) + 2); 59 | ++n; 60 | } 61 | 62 | switch (n) { 63 | case 1: 64 | return sc::Base64DecodeErrorCode::not_enough_tokens; 65 | case 2: 66 | return (val << 6) | (1 & 0x03); 67 | case 3: 68 | return (val << 6) | (2 & 0x03); 69 | default: 70 | return (val << 6) | (3 & 0x03); 71 | } 72 | } 73 | 74 | } // namespace 75 | 76 | namespace sc 77 | { 78 | auto base64_decode(std::string_view encoded) 79 | -> Result, Base64DecodeErrorCode> 80 | { 81 | using std::advance; 82 | using std::begin; 83 | using std::copy_n; 84 | using std::distance; 85 | using std::end; 86 | 87 | std::vector result; 88 | std::array decode_chunk {}; 89 | 90 | auto input_pos = begin(encoded); 91 | 92 | while (input_pos != end(encoded)) { 93 | auto const num_to_read = std::min( 94 | decode_chunk.size(), 95 | static_cast(distance(input_pos, end(encoded)))); 96 | auto out_pos = copy_n(input_pos, num_to_read, begin(decode_chunk)); 97 | 98 | while (out_pos != end(decode_chunk)) 99 | *out_pos++ = '='; 100 | 101 | advance(input_pos, num_to_read); 102 | auto decode_result = base64_decode_chunk( 103 | std::string_view { decode_chunk.data(), decode_chunk.size() }); 104 | if (!decode_result) 105 | return sc::get_error(decode_result); 106 | 107 | auto val = *decode_result; 108 | auto const num_bytes = val & 0x03; 109 | val &= ~0x03; 110 | 111 | std::span const bytes { reinterpret_cast(&val), 112 | sizeof(val) }; 113 | 114 | auto begin = begin_by_byte_order(bytes); 115 | 116 | std::copy_n(begin, num_bytes, std::back_inserter(result)); 117 | } 118 | 119 | return result; 120 | } 121 | } // namespace sc 122 | -------------------------------------------------------------------------------- /src/utils/base64.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_UTILS_BASE64_HPP_INCLUDED 2 | #define SHADOW_CAST_UTILS_BASE64_HPP_INCLUDED 3 | 4 | #include "result.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace sc 13 | { 14 | 15 | enum struct Base64DecodeErrorCode 16 | { 17 | invalid_token, 18 | not_enough_tokens 19 | }; 20 | 21 | [[nodiscard]] auto base64_decode(std::string_view encoded) 22 | -> Result, Base64DecodeErrorCode>; 23 | } // namespace sc 24 | 25 | #endif // SHADOW_CAST_UTILS_BASE64_HPP_INCLUDED 26 | -------------------------------------------------------------------------------- /src/utils/borrowed_ptr.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_BORROWED_PTR_HPP_INCLUDED 2 | #define SHADOW_CAST_BORROWED_PTR_HPP_INCLUDED 3 | 4 | #include 5 | 6 | namespace sc 7 | { 8 | 9 | template 10 | struct BorrowedPtr 11 | { 12 | using value_type = std::remove_reference_t; 13 | using pointer = value_type*; 14 | using const_pointer = value_type const*; 15 | using reference = value_type&; 16 | using const_reference = value_type const&; 17 | 18 | BorrowedPtr() = default; 19 | 20 | template 21 | BorrowedPtr(U* p) 22 | requires(std::is_convertible_v) 23 | : ptr_ { p } 24 | { 25 | } 26 | 27 | template 28 | BorrowedPtr(BorrowedPtr const& other) 29 | requires(std::is_convertible_v::pointer, pointer>) 30 | : ptr_ { other.get() } 31 | { 32 | } 33 | 34 | auto get() const noexcept -> pointer { return ptr_; } 35 | 36 | operator bool() const noexcept { return ptr_ != nullptr; } 37 | 38 | auto operator->() const noexcept -> const_pointer { return ptr_; } 39 | 40 | auto operator->() noexcept -> pointer { return ptr_; } 41 | 42 | auto operator*() const noexcept -> const_reference { return *ptr_; } 43 | 44 | auto operator*() noexcept -> reference { return *ptr_; } 45 | 46 | private: 47 | pointer ptr_ = nullptr; 48 | }; 49 | 50 | } // namespace sc 51 | 52 | #endif // SHADOW_CAST_BORROWED_PTR_HPP_INCLUDED 53 | -------------------------------------------------------------------------------- /src/utils/contracts.cpp: -------------------------------------------------------------------------------- 1 | #include "utils/contracts.hpp" 2 | #include 3 | #include 4 | 5 | namespace sc 6 | { 7 | auto contract_check_failed(char const* msg, std::source_location loc) -> void 8 | { 9 | std::fprintf(stderr, 10 | "Contract check failed: %s - %s:%u\n", 11 | msg, 12 | loc.file_name(), 13 | loc.line()); 14 | std::terminate(); 15 | } 16 | } // namespace sc 17 | -------------------------------------------------------------------------------- /src/utils/contracts.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_UTILS_CONTRACTS_HPP_INCLUDED 2 | #define SHADOW_CAST_UTILS_CONTRACTS_HPP_INCLUDED 3 | 4 | #include 5 | 6 | #define SC_STRINGIFY_IMPL(s) #s 7 | #define SC_STRINGIFY(s) SC_STRINGIFY_IMPL(s) 8 | #define SC_EXPECT(cond) \ 9 | if (!(cond)) \ 10 | ::sc::contract_check_failed(SC_STRINGIFY(cond)) 11 | 12 | namespace sc 13 | { 14 | 15 | [[noreturn]] auto contract_check_failed( 16 | char const* /*msg*/, 17 | std::source_location /*loc*/ = std::source_location::current()) -> void; 18 | 19 | } // namespace sc 20 | 21 | #endif // SHADOW_CAST_UTILS_CONTRACTS_HPP_INCLUDED 22 | -------------------------------------------------------------------------------- /src/utils/elapsed.cpp: -------------------------------------------------------------------------------- 1 | #include "utils/elapsed.hpp" 2 | 3 | namespace sc 4 | { 5 | 6 | auto Elapsed::value() const noexcept -> std::uint64_t 7 | { 8 | namespace ch = std::chrono; 9 | 10 | auto const now = ch::steady_clock::now(); 11 | 12 | return ch::duration_cast(now - start_).count(); 13 | } 14 | 15 | auto Elapsed::nanosecond_value() const noexcept -> std::uint64_t 16 | { 17 | namespace ch = std::chrono; 18 | 19 | auto const now = ch::steady_clock::now(); 20 | 21 | return ch::duration_cast(now - start_).count(); 22 | } 23 | 24 | auto Elapsed::reset() noexcept -> void 25 | { 26 | start_ = std::chrono::steady_clock::now(); 27 | } 28 | 29 | } // namespace sc 30 | -------------------------------------------------------------------------------- /src/utils/elapsed.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_ELAPSED_HPP_INCLUDED 2 | #define SHADOW_CAST_ELAPSED_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | 7 | namespace sc 8 | { 9 | 10 | struct Elapsed 11 | { 12 | auto value() const noexcept -> std::uint64_t; 13 | auto nanosecond_value() const noexcept -> std::uint64_t; 14 | auto reset() noexcept -> void; 15 | 16 | private: 17 | std::chrono::time_point start_ = 18 | std::chrono::steady_clock::now(); 19 | }; 20 | 21 | Elapsed const global_elapsed; 22 | } // namespace sc 23 | 24 | #endif // SHADOW_CAST_ELAPSED_HPP_INCLUDED 25 | -------------------------------------------------------------------------------- /src/utils/frame_time.cpp: -------------------------------------------------------------------------------- 1 | #include "utils/frame_time.hpp" 2 | #include 3 | 4 | namespace 5 | { 6 | std::uint64_t constexpr kNsPerSec = 1'000'000'000; 7 | std::uint64_t constexpr kNsPerMs = 1'000'000; 8 | auto constexpr kRound = kNsPerMs / 2; 9 | 10 | } // namespace 11 | namespace sc 12 | { 13 | 14 | FrameTime::FrameTime(std::uint64_t frame_time_nanoseconds) noexcept 15 | : frame_time_nanoseconds_ { frame_time_nanoseconds } 16 | { 17 | } 18 | 19 | auto FrameTime::value() const noexcept -> std::uint64_t 20 | { 21 | return frame_time_nanoseconds_; 22 | } 23 | 24 | auto FrameTime::value_in_milliseconds() const noexcept -> std::uint64_t 25 | { 26 | return (frame_time_nanoseconds_ + kRound) / kNsPerMs; 27 | } 28 | 29 | auto FrameTime::fps() const noexcept -> float 30 | { 31 | return static_cast(kNsPerSec) / frame_time_nanoseconds_; 32 | } 33 | 34 | auto FrameTime::fps_ratio() const noexcept -> AVRational 35 | { 36 | return av_make_q(1, static_cast(fps())); 37 | } 38 | 39 | auto truncate_to_millisecond(FrameTime const& ft) noexcept -> FrameTime 40 | { 41 | auto const in_ms = ft.value() / kNsPerMs; 42 | return FrameTime { in_ms * kNsPerMs }; 43 | } 44 | auto from_fps(std::uint32_t fps) noexcept -> FrameTime 45 | { 46 | return FrameTime { kNsPerSec / fps }; 47 | } 48 | 49 | } // namespace sc 50 | -------------------------------------------------------------------------------- /src/utils/frame_time.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_UTILS_FRAME_TIME_HPP_INCLUDED 2 | #define SHADOW_CAST_UTILS_FRAME_TIME_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | 7 | namespace sc 8 | { 9 | struct FrameTime 10 | { 11 | explicit FrameTime(std::uint64_t) noexcept; 12 | auto value() const noexcept -> std::uint64_t; 13 | auto value_in_milliseconds() const noexcept -> std::uint64_t; 14 | auto fps() const noexcept -> float; 15 | auto fps_ratio() const noexcept -> AVRational; 16 | 17 | private: 18 | std::uint64_t frame_time_nanoseconds_; 19 | }; 20 | 21 | auto truncate_to_millisecond(FrameTime const&) noexcept -> FrameTime; 22 | auto from_fps(std::uint32_t) noexcept -> FrameTime; 23 | 24 | } // namespace sc 25 | 26 | #endif // SHADOW_CAST_UTILS_FRAME_TIME_HPP_INCLUDED 27 | -------------------------------------------------------------------------------- /src/utils/non_pointer.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_UTILS_NON_POINTER_HPP_INCLUDED 2 | #define SHADOW_CAST_UTILS_NON_POINTER_HPP_INCLUDED 3 | 4 | #include 5 | 6 | namespace sc 7 | { 8 | template 9 | struct NonPointer 10 | { 11 | static T constexpr kNull = static_cast(-1); 12 | 13 | NonPointer() noexcept = default; 14 | 15 | NonPointer(T val) noexcept 16 | : val_ { val } 17 | { 18 | } 19 | 20 | NonPointer(std::nullptr_t) noexcept 21 | : val_ { kNull } 22 | { 23 | } 24 | 25 | operator T() noexcept { return val_; } 26 | 27 | auto operator==(NonPointer const& other) noexcept -> bool 28 | { 29 | return val_ == other.val_; 30 | } 31 | 32 | auto operator!=(NonPointer const& other) noexcept -> bool 33 | { 34 | return !(*this == other); 35 | } 36 | 37 | auto operator==(std::nullptr_t) noexcept -> bool { return val_ == kNull; } 38 | auto operator!=(std::nullptr_t) noexcept -> bool { return val_ != kNull; } 39 | 40 | private: 41 | T val_ { kNull }; 42 | }; 43 | 44 | } // namespace sc 45 | #endif // SHADOW_CAST_UTILS_NON_POINTER_HPP_INCLUDED 46 | -------------------------------------------------------------------------------- /src/utils/result.cpp: -------------------------------------------------------------------------------- 1 | #include "utils/result.hpp" 2 | 3 | namespace sc 4 | { 5 | auto result_ok() noexcept -> detail::VoidResult 6 | { 7 | return detail::VoidResult {}; 8 | } 9 | } // namespace sc 10 | -------------------------------------------------------------------------------- /src/utils/scope_guard.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_UTILS_SCOPE_GUARD_HPP_INCLUDED 2 | #define SHADOW_CAST_UTILS_SCOPE_GUARD_HPP_INCLUDED 3 | 4 | #include 5 | 6 | #define SC_GEN_VAR_NAME_IMPL(pfx, line) pfx##line 7 | #define SC_GEN_VAR_NAME(pfx, line) SC_GEN_VAR_NAME_IMPL(pfx, line) 8 | #define SC_SCOPE_GUARD(fn) \ 9 | auto SC_GEN_VAR_NAME(scope_guard_, __LINE__) = ::sc::ScopeGuard \ 10 | { \ 11 | fn \ 12 | } 13 | 14 | namespace sc 15 | { 16 | 17 | template 18 | struct ScopeGuard 19 | { 20 | explicit ScopeGuard(F&& f) noexcept 21 | : fn { std::forward(f) } 22 | { 23 | } 24 | 25 | ~ScopeGuard() 26 | { 27 | if (active) 28 | fn(); 29 | } 30 | 31 | ScopeGuard(ScopeGuard const&) = delete; 32 | auto operator=(ScopeGuard const&) -> ScopeGuard& = delete; 33 | ScopeGuard(ScopeGuard&&) = delete; 34 | auto operator=(ScopeGuard&&) -> ScopeGuard& = delete; 35 | auto deactivate() noexcept -> void { active = false; } 36 | 37 | F fn; 38 | bool active { true }; 39 | }; 40 | 41 | } // namespace sc 42 | 43 | #endif // SHADOW_CAST_UTILS_SCOPE_GUARD_HPP_INCLUDED 44 | -------------------------------------------------------------------------------- /src/utils/symbol.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SHADOW_CAST_UTILS_SYMBOL_HPP_INCLUDED 2 | #define SHADOW_CAST_UTILS_SYMBOL_HPP_INCLUDED 3 | 4 | #include "error.hpp" 5 | #include 6 | 7 | #define TRY_ATTACH_SYMBOL(target, name, lib) \ 8 | ::sc::attach_symbol(target, name, lib) 9 | 10 | namespace sc 11 | { 12 | template 13 | auto attach_symbol(F** fn, char const* name, void* lib) -> void 14 | { 15 | *fn = reinterpret_cast(dlsym(lib, name)); 16 | if (!*fn) 17 | throw sc::ModuleError { std::string { "Couldn't load symbol: " } + 18 | name }; 19 | } 20 | 21 | } // namespace sc 22 | 23 | #endif // SHADOW_CAST_UTILS_SYMBOL_HPP_INCLUDED 24 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(MakeTest) 2 | include_directories(${PROJECT_SOURCE_DIR}/src) 3 | 4 | add_library(testing OBJECT testing.cpp) 5 | 6 | # NOTE: 7 | # Use -DSHADOW_CAST_ENABLE_TEST_CATEGORIES="val1;val2" to control the 8 | # ENABLE_IF flags 9 | 10 | add_subdirectory(glsl) 11 | 12 | make_test(NAME base64_tests SOURCES base64_tests.cpp) 13 | make_test(NAME intrusive_list_tests SOURCES intrusive_list_tests.cpp) 14 | make_test(NAME pool_tests SOURCES pool_tests.cpp) 15 | make_test(NAME cmd_line_tests SOURCES cmd_line_tests.cpp) 16 | make_test( 17 | NAME gl_shader_tests 18 | SOURCES gl_shader_tests.cpp 19 | ENABLE_IF wayland all 20 | LABELS wayland) 21 | make_test( 22 | NAME gl_buffer_tests 23 | SOURCES gl_buffer_tests.cpp 24 | ENABLE_IF wayland all 25 | LABELS wayland) 26 | make_test(NAME gl_error_tests SOURCES gl_error_tests.cpp) 27 | make_test( 28 | NAME gl_texture_tests 29 | SOURCES gl_texture_tests.cpp 30 | ENABLE_IF wayland all 31 | LABELS wayland) 32 | make_test(NAME histogram_tests SOURCES histogram_tests.cpp) 33 | 34 | add_custom_target( 35 | pixel_data 36 | COMMAND cmake 37 | -E copy_if_different 38 | "${CMAKE_CURRENT_LIST_DIR}/data/cyberpunk-girl-rgb-640x640.rgb" 39 | "${CMAKE_CURRENT_BINARY_DIR}/data/cyberpunk-girl-rgb-640x640.rgb" 40 | ) 41 | 42 | make_test( 43 | NAME gl_render_to_texture_test 44 | SOURCES gl_render_to_texture_test.cpp 45 | ENV_VARS 46 | "SHADOW_CAST_TEST_PIXEL_DATA_FILE=${CMAKE_CURRENT_BINARY_DIR}/data/cyberpunk-girl-rgb-640x640.rgb" 47 | ENABLE_IF wayland all 48 | LABELS wayland 49 | LINK_LIBRARIES glsl_sources) 50 | 51 | add_dependencies(gl_render_to_texture_test pixel_data) 52 | 53 | if(EXISTS ${PROJECT_SOURCE_DIR}/.private/nvfbc.key) 54 | file(STRINGS ${PROJECT_SOURCE_DIR}/.private/nvfbc.key SHADOW_CAST_NVFBC_KEY) 55 | endif() 56 | 57 | if(SHADOW_CAST_NVFBC_KEY) 58 | make_test( 59 | NAME nvidia_tests 60 | SOURCES nvidia_tests.cpp 61 | ENV_VARS 62 | "SHADOW_CAST_NVFBC_KEY=${SHADOW_CAST_NVFBC_KEY}" 63 | # ASAN needs the following runtime setting to prevent OOM when invoking `cuInit` 64 | # (ref: https://github.com/google/sanitizers/issues/629#issuecomment-161357276) 65 | "ASAN_OPTIONS=protect_shadow_gap=0" 66 | ENABLE_IF nvfbc all 67 | LABELS nvidia 68 | ) 69 | else() 70 | message(WARNING "Couldn't load nvfbc.key. Ignoring nvidia tests") 71 | endif() 72 | 73 | -------------------------------------------------------------------------------- /tests/base64_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "./testing.hpp" 2 | #include "utils.hpp" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | auto should_decode() 12 | { 13 | std::string_view const input = "bGlnaHQgd29yay4="; 14 | std::string_view const expected_output = "light work."; 15 | 16 | auto const decode_result = sc::base64_decode(input); 17 | EXPECT(decode_result); 18 | 19 | for (auto const& b : *decode_result) 20 | std::cerr << int(b) << ','; 21 | std::cerr << '\n'; 22 | 23 | std::string_view const output { reinterpret_cast( 24 | sc::get_value(decode_result).data()), 25 | sc::get_value(decode_result).size() }; 26 | std::cerr << output << '\n'; 27 | EXPECT(output == expected_output); 28 | } 29 | 30 | auto should_fail_to_decode_when_not_enough_tokens() 31 | { 32 | std::string_view const input = "b"; 33 | 34 | auto const decode_result = sc::base64_decode(input); 35 | EXPECT(!decode_result); 36 | EXPECT(sc::get_error(decode_result) == 37 | sc::Base64DecodeErrorCode::not_enough_tokens); 38 | } 39 | 40 | auto should_fail_to_decode_when_input_contains_invalid_token() 41 | { 42 | std::string_view const input = "QQ%="; 43 | 44 | auto const decode_result = sc::base64_decode(input); 45 | EXPECT(!decode_result); 46 | EXPECT(sc::get_error(decode_result) == 47 | sc::Base64DecodeErrorCode::invalid_token); 48 | } 49 | 50 | auto main() -> int 51 | { 52 | /* Tests... 53 | */ 54 | return testing::run( 55 | { TEST(should_decode), 56 | TEST(should_fail_to_decode_when_not_enough_tokens), 57 | TEST(should_fail_to_decode_when_input_contains_invalid_token) }); 58 | } 59 | -------------------------------------------------------------------------------- /tests/cmd_line_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "testing.hpp" 2 | #include "utils/cmd_line.hpp" 3 | 4 | auto should_parse() -> void 5 | { 6 | char const* argv[] = { "-V", "h264_nvenc", "-f30", "-A", 7 | "libopus", "/tmp/test.mp4", "-h", "-v" }; 8 | 9 | auto const cmdline = sc::parse_cmd_line(std::size(argv), argv); 10 | 11 | EXPECT(cmdline.args().size() == 1); 12 | 13 | EXPECT(cmdline.has_option(sc::CmdLineOption::frame_rate)); 14 | EXPECT(cmdline.get_option_value(sc::CmdLineOption::frame_rate, 15 | sc::number_value) == 30); 16 | 17 | EXPECT(cmdline.has_option(sc::CmdLineOption::video_encoder)); 18 | EXPECT(cmdline.get_option_value(sc::CmdLineOption::video_encoder) == 19 | "h264_nvenc"); 20 | 21 | EXPECT(cmdline.has_option(sc::CmdLineOption::audio_encoder)); 22 | EXPECT(cmdline.get_option_value(sc::CmdLineOption::audio_encoder) == 23 | "libopus"); 24 | 25 | EXPECT(cmdline.has_option(sc::CmdLineOption::help)); 26 | 27 | EXPECT(cmdline.has_option(sc::CmdLineOption::version)); 28 | 29 | EXPECT(cmdline.args()[0] == "/tmp/test.mp4"); 30 | } 31 | 32 | auto should_fail_number_range() -> void 33 | { 34 | char const* argv[] = { "-f-1", "/tmp/test.mp4" }; 35 | 36 | EXPECT_THROWS(sc::parse_cmd_line(std::size(argv), argv)); 37 | } 38 | 39 | auto main() -> int 40 | { 41 | return testing::run({ TEST(should_parse), TEST(should_fail_number_range) }); 42 | } 43 | -------------------------------------------------------------------------------- /tests/data/cyberpunk-girl-rgb-640x640.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmbeard/shadow-cast/6880aa157f97fc0b38581c6375fc9af35fbc7641/tests/data/cyberpunk-girl-rgb-640x640.rgb -------------------------------------------------------------------------------- /tests/gl_buffer_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "gl/buffer.hpp" 2 | #include "gl/object.hpp" 3 | #include "gl/vertex_array_object.hpp" 4 | #include "platform/egl.hpp" 5 | #include "platform/wayland.hpp" 6 | #include "testing.hpp" 7 | #include "utils/scope_guard.hpp" 8 | #include 9 | #include 10 | 11 | namespace ogl = sc::opengl; 12 | 13 | auto should_bind_vao() -> void 14 | { 15 | auto vao = ogl::create(); 16 | 17 | ogl::bind(ogl::vertex_array_target, vao, [](auto binding) { 18 | ogl::enable_vertex_array_attrib(binding, 1); 19 | }); 20 | } 21 | 22 | auto should_bind_and_unbind_buffer() -> void 23 | { 24 | auto buffer = ogl::create(); 25 | auto binding = ogl::bind(ogl::array_buffer_target, buffer); 26 | } 27 | 28 | auto should_buffer_data() -> void 29 | { 30 | auto vao = ogl::create(); 31 | auto buffer = ogl::create(); 32 | ogl::bind(ogl::vertex_array_target, vao, [&](auto vao_binding) { 33 | std::array const data = { 1.f, 2.f, 3.f, 4.f }; 34 | 35 | auto binding = ogl::bind(ogl::array_buffer_target, buffer); 36 | ogl::buffer_data( 37 | binding, std::span { data.data(), data.size() }, GL_STATIC_DRAW); 38 | 39 | ogl::vertex_attrib_pointer(binding, 0, 1, GL_FLOAT, false, 0, 0); 40 | ogl::enable_vertex_array_attrib(vao_binding, 0); 41 | }); 42 | } 43 | 44 | auto main() -> int 45 | { 46 | sc::wayland::DisplayPtr wayland_display { wl_display_connect(nullptr) }; 47 | EXPECT(wayland_display); 48 | auto wayland = sc::initialize_wayland(std::move(wayland_display)); 49 | sc::initialize_wayland_egl(sc::egl(), *wayland); 50 | return testing::run({ TEST(should_bind_vao), 51 | TEST(should_bind_and_unbind_buffer), 52 | TEST(should_buffer_data) }); 53 | } 54 | -------------------------------------------------------------------------------- /tests/gl_error_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "gl/error.hpp" 2 | #include "testing.hpp" 3 | #include 4 | #include 5 | 6 | auto should_yield_correct_error_string() -> void 7 | { 8 | EXPECT(std::string_view { sc::gl_error_to_string(GL_INVALID_ENUM) } == 9 | "GL_INVALID_ENUM"); 10 | } 11 | 12 | auto main() -> int 13 | { 14 | return testing::run({ TEST(should_yield_correct_error_string) }); 15 | } 16 | -------------------------------------------------------------------------------- /tests/gl_shader_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "gl/object.hpp" 2 | #include "gl/program.hpp" 3 | #include "gl/shader.hpp" 4 | #include "platform/egl.hpp" 5 | #include "platform/wayland.hpp" 6 | #include "testing.hpp" 7 | #include 8 | 9 | constexpr std::string_view vert_src = R"~( 10 | #version 330 core 11 | layout (location = 0) in vec3 aPos; 12 | out vec4 vertexColor; 13 | uniform vec3 light_direction; 14 | void main() 15 | { 16 | gl_Position = vec4(aPos, 1.0); 17 | vertexColor = vec4(0.5, 0.0, 0.0, 1.0) * 18 | vec4(light_direction.xyz, 0.0); 19 | })~"; 20 | 21 | constexpr std::string_view frag_src = R"~(#version 330 core 22 | out vec4 FragColor; 23 | in vec4 vertexColor; 24 | void main() 25 | { 26 | FragColor = vertexColor; 27 | })~"; 28 | 29 | namespace ogl = sc::opengl; 30 | 31 | auto should_create_shader() -> void 32 | { 33 | ogl::Shader shader_a = ogl::create_shader(ogl::ShaderType::fragment); 34 | ogl::Shader shader_b = ogl::create_shader(ogl::ShaderType::fragment); 35 | EXPECT(shader_a.name()); 36 | EXPECT(shader_b.name()); 37 | } 38 | 39 | auto should_compile_simple_shader() -> void 40 | { 41 | ogl::Shader shader = ogl::create_shader(ogl::ShaderType::fragment); 42 | shader_source(shader, frag_src); 43 | compile_shader(shader); 44 | } 45 | 46 | auto should_create_program() -> void 47 | { 48 | auto prog_ = ogl::create(); 49 | prog_.num_shaders_attached = 42; 50 | 51 | ogl::Program prog { std::move(prog_) }; 52 | EXPECT(prog.name()); 53 | EXPECT(prog.num_shaders_attached == 42); 54 | } 55 | 56 | auto should_link_program() -> void 57 | { 58 | auto vert = ogl::create_shader(ogl::ShaderType::vertex); 59 | auto frag = ogl::create_shader(ogl::ShaderType::fragment); 60 | 61 | shader_source(vert, vert_src); 62 | shader_source(frag, frag_src); 63 | 64 | auto prog = ogl::create(); 65 | EXPECT(prog.name()); 66 | 67 | sc::opengl::attach_shader(prog, vert); 68 | sc::opengl::attach_shader(prog, frag); 69 | sc::opengl::link_program(prog); 70 | sc::opengl::validate_program(prog); 71 | } 72 | 73 | auto should_get_uniform_location() -> void 74 | { 75 | auto vert = ogl::create_shader(ogl::ShaderType::vertex); 76 | auto frag = ogl::create_shader(ogl::ShaderType::fragment); 77 | 78 | shader_source(vert, vert_src); 79 | shader_source(frag, frag_src); 80 | 81 | auto prog = ogl::create(); 82 | EXPECT(prog.name()); 83 | 84 | sc::opengl::attach_shader(prog, vert); 85 | sc::opengl::attach_shader(prog, frag); 86 | sc::opengl::link_program(prog); 87 | sc::opengl::validate_program(prog); 88 | 89 | EXPECT(sc::opengl::get_uniform_location(prog, "light_direction") >= 0); 90 | } 91 | 92 | auto main() -> int 93 | { 94 | sc::wayland::DisplayPtr wayland_display { wl_display_connect(nullptr) }; 95 | EXPECT(wayland_display); 96 | auto wayland = sc::initialize_wayland(std::move(wayland_display)); 97 | sc::initialize_wayland_egl(sc::egl(), *wayland); 98 | return testing::run({ TEST(should_create_shader), 99 | TEST(should_compile_simple_shader), 100 | TEST(should_link_program), 101 | TEST(should_get_uniform_location), 102 | TEST(should_create_program) }); 103 | } 104 | -------------------------------------------------------------------------------- /tests/gl_texture_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "gl/object.hpp" 2 | #include "gl/texture.hpp" 3 | #include "platform/egl.hpp" 4 | #include "platform/wayland.hpp" 5 | #include "testing.hpp" 6 | #include 7 | #include 8 | 9 | namespace ogl = sc::opengl; 10 | 11 | auto should_create_texture() -> void 12 | { 13 | auto tex = sc::opengl::create(); 14 | EXPECT(tex.name() != 0); 15 | } 16 | 17 | auto should_move_texture() -> void 18 | { 19 | auto tex1 = ogl::create(); 20 | auto const id = tex1.name(); 21 | 22 | ogl::Texture tex2 { std::move(tex1) }; 23 | EXPECT(tex2.name() == id); 24 | EXPECT(tex1.name() == 0); 25 | } 26 | 27 | auto should_bind_texture() -> void 28 | { 29 | auto tex = ogl::create(); 30 | auto binding = ogl::bind(ogl::texture_2d_target, tex); 31 | EXPECT(binding.is_bound()); 32 | EXPECT(binding.name() == tex.name()); 33 | } 34 | 35 | auto should_initialize_texture_data() -> void 36 | { 37 | auto tex = ogl::create(); 38 | ogl::bind(ogl::texture_2d_target, tex, [](auto binding) { 39 | ogl::texture_image_2d(binding, 40 | 0, 41 | GL_RGBA, 42 | 256, 43 | 256, 44 | 0, 45 | GL_RGBA, 46 | GL_UNSIGNED_BYTE, 47 | nullptr); 48 | ogl::texture_parameter(binding, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 49 | ogl::texture_parameter(binding, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 50 | std::array swizzle { GL_BLUE, GL_GREEN, GL_RED, GL_ALPHA }; 51 | ogl::texture_parameter(binding, 52 | GL_TEXTURE_SWIZZLE_RGBA, 53 | std::span { swizzle.data(), swizzle.size() }); 54 | }); 55 | } 56 | 57 | auto main() -> int 58 | { 59 | sc::wayland::DisplayPtr wayland_display { wl_display_connect(nullptr) }; 60 | EXPECT(wayland_display); 61 | auto wayland = sc::initialize_wayland(std::move(wayland_display)); 62 | sc::initialize_wayland_egl(sc::egl(), *wayland); 63 | return testing::run({ TEST(should_create_texture), 64 | TEST(should_initialize_texture_data), 65 | TEST(should_bind_texture), 66 | TEST(should_move_texture) }); 67 | } 68 | -------------------------------------------------------------------------------- /tests/glsl/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(EmbeddedGLSLTarget) 2 | 3 | add_embedded_glsl_target( 4 | NAME glsl_sources 5 | SOURCES 6 | flipped_y_vertex.glsl 7 | identity_fragment.glsl 8 | textured_vertex.glsl 9 | textured_brightness_fragment.glsl 10 | ) 11 | -------------------------------------------------------------------------------- /tests/glsl/flipped_y_vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | layout (location = 0) in vec3 aPos; 3 | layout (location = 1) in vec3 aColor; 4 | 5 | out vec4 color; 6 | 7 | void main() 8 | { 9 | gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0) * 10 | vec4(1.0, -1.0, 1.0, 1.0); 11 | 12 | color = vec4(aColor, 1.0); 13 | } 14 | 15 | // vim: ft=glsl 16 | -------------------------------------------------------------------------------- /tests/glsl/identity_fragment.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | out vec4 FragColor; 3 | 4 | in vec4 color; 5 | 6 | void main() 7 | { 8 | FragColor = color; 9 | } 10 | // vim: ft=glsl 11 | -------------------------------------------------------------------------------- /tests/glsl/textured_brightness_fragment.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | out vec4 FragColor; 3 | 4 | in vec2 tex_coord; 5 | 6 | uniform vec2 dimensions; 7 | uniform sampler2D texture_sampler; 8 | 9 | const float brightness_scale = 1.5; 10 | const float border_size = 5.0; 11 | const float border_radius = 100.0; 12 | 13 | float get_border_min(float pos) 14 | { 15 | return 1.0 * smoothstep(border_size, border_size + border_radius, pos); 16 | } 17 | 18 | float get_border_max(float pos) 19 | { 20 | return 1.0 * smoothstep(pos, pos + border_radius, dimensions.x - border_size); 21 | } 22 | 23 | void main() 24 | { 25 | float border_left_color = get_border_min(gl_FragCoord.x); 26 | float border_right_color = get_border_max(gl_FragCoord.x); 27 | float border_top_color = get_border_min(gl_FragCoord.y); 28 | float border_bottom_color = get_border_max(gl_FragCoord.y); 29 | 30 | vec3 color = 31 | texture(texture_sampler, tex_coord).rgb 32 | * clamp(border_left_color 33 | * border_right_color 34 | * border_top_color 35 | * border_bottom_color, 0.0, 1.0); 36 | vec3 cell_color = step(0.5, texture(texture_sampler, tex_coord).rgb); 37 | FragColor = vec4(mix(color, cell_color, 0.25), 1.0) * brightness_scale; 38 | } 39 | 40 | // vim: ft=glsl 41 | -------------------------------------------------------------------------------- /tests/glsl/textured_vertex.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | layout (location = 0) in vec3 aPos; 3 | layout (location = 1) in vec2 aTexCoord; 4 | 5 | out vec2 tex_coord; 6 | 7 | void main() 8 | { 9 | gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); 10 | tex_coord = aTexCoord; 11 | } 12 | 13 | // vim: ft=glsl 14 | -------------------------------------------------------------------------------- /tests/histogram_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "metrics/formatting.hpp" 2 | #include "metrics/histogram.hpp" 3 | #include "testing.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | using sc::metrics::Histogram; 10 | 11 | namespace 12 | { 13 | template 14 | auto count_histogram_samples(Histogram const& data) noexcept -> T 15 | { 16 | return std::accumulate( 17 | std::begin(data), std::end(data), T {}, [](auto&& a, auto&& b) { 18 | return a + std::get<1>(b); 19 | }); 20 | } 21 | 22 | } // namespace 23 | 24 | auto should_construct_empty() -> void 25 | { 26 | /* NOTE: 27 | * This creates a histogram with `[1, 2, 3, 4, 5]`, which is 28 | * effectively `[ 0<1, 1<2, 2<3, 3<4, 4<5 ]`... 29 | */ 30 | Histogram histogram; 31 | 32 | EXPECT(count_histogram_samples(histogram) == 0); 33 | } 34 | 35 | auto should_add_value() -> void 36 | { 37 | Histogram histogram; 38 | 39 | histogram.add_value(3); 40 | 41 | std::for_each(histogram.begin(), histogram.end(), [](auto&& val) { 42 | std::cerr << std::get<0>(val) << ": " << std::get<1>(val) << '\n'; 43 | }); 44 | 45 | EXPECT(std::get<1>(histogram[2]) == 1); 46 | } 47 | 48 | auto should_add_multiple_values() -> void 49 | { 50 | Histogram histogram; 51 | 52 | histogram.add_value(3); 53 | histogram.add_value(3); 54 | histogram.add_value(3); 55 | histogram.add_value(3); 56 | 57 | histogram.add_value(10); 58 | 59 | std::for_each(histogram.begin(), histogram.end(), [](auto&& val) { 60 | std::cerr << std::get<0>(val) << ": " << std::get<1>(val) << '\n'; 61 | }); 62 | 63 | EXPECT(std::get<1>(histogram[2]) == 4); 64 | EXPECT(std::get<1>(histogram[4]) == 1); 65 | } 66 | 67 | auto should_contain_the_correc_total_samples() -> void 68 | { 69 | Histogram histogram; 70 | 71 | constexpr std::size_t N = 10; 72 | 73 | EXPECT(count_histogram_samples(histogram) == 0); 74 | 75 | for ([[maybe_unused]] auto&& _ : std::array {}) { 76 | histogram.add_value(42); 77 | } 78 | 79 | std::for_each(histogram.begin(), histogram.end(), [](auto&& val) { 80 | std::cerr << std::get<0>(val) << ": " << std::get<1>(val) << '\n'; 81 | }); 82 | 83 | EXPECT(count_histogram_samples(histogram) == N); 84 | } 85 | 86 | auto should_print_histogram() -> void 87 | { 88 | Histogram histogram; 89 | 90 | sc::metrics::format_histogram( 91 | std::cerr, histogram, "Frame-time Nanoseconds", "Test Histogram"); 92 | } 93 | 94 | auto main() -> int 95 | { 96 | return testing::run({ TEST(should_add_value), 97 | TEST(should_add_multiple_values), 98 | TEST(should_construct_empty), 99 | TEST(should_contain_the_correc_total_samples), 100 | TEST(should_print_histogram) }); 101 | } 102 | -------------------------------------------------------------------------------- /tests/pool_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "testing.hpp" 2 | #include "utils/intrusive_list.hpp" 3 | #include "utils/pool.hpp" 4 | 5 | struct Identity : sc::ListItemBase 6 | { 7 | }; 8 | 9 | auto should_get_and_put() -> void 10 | { 11 | sc::Pool pool; 12 | 13 | for (auto i = 0; i < 10; ++i) { 14 | [[maybe_unused]] auto p = pool.get(); 15 | } 16 | } 17 | 18 | auto main() -> int { return testing::run({ TEST(should_get_and_put) }); } 19 | -------------------------------------------------------------------------------- /tests/testing.cpp: -------------------------------------------------------------------------------- 1 | #include "./testing.hpp" 2 | #include 3 | #include 4 | 5 | namespace testing 6 | { 7 | 8 | TestFailure::TestFailure(char const* msg) 9 | : std::logic_error { msg } 10 | { 11 | } 12 | 13 | TestIgnored::TestIgnored(char const* msg) 14 | : std::logic_error { msg } 15 | { 16 | } 17 | 18 | auto run(std::initializer_list tests) -> int 19 | { 20 | std::size_t passed = 0; 21 | for (auto const& test : tests) { 22 | try { 23 | std::get<1>(test)(); 24 | ++passed; 25 | } 26 | catch (TestIgnored const& e) { 27 | std::cerr << std::get<0>(test) << " ignored: " << e.what() << '\n'; 28 | } 29 | catch (std::exception const& e) { 30 | std::cerr << std::get<0>(test) << " failed: " << e.what() << '\n'; 31 | } 32 | } 33 | 34 | std::cerr << (tests.size() - passed) << "/" << tests.size() 35 | << " tests failed\n"; 36 | 37 | return passed == tests.size() ? 0 : 1; 38 | } 39 | 40 | auto run_with_context(int argc, 41 | char const** argv, 42 | std::initializer_list tests) -> int 43 | { 44 | std::size_t passed = 0; 45 | for (auto const& test : tests) { 46 | try { 47 | std::get<1>(test)(TestContext { argc > 0 ? argc - 1 : 0, 48 | argc > 0 ? argv + 1 : argv }); 49 | ++passed; 50 | } 51 | catch (TestIgnored const& e) { 52 | std::cerr << std::get<0>(test) << " ignored: " << e.what() << '\n'; 53 | } 54 | catch (std::exception const& e) { 55 | std::cerr << std::get<0>(test) << " failed: " << e.what() << '\n'; 56 | } 57 | } 58 | 59 | std::cerr << (tests.size() - passed) << "/" << tests.size() 60 | << " tests failed\n"; 61 | 62 | return passed == tests.size() ? 0 : 1; 63 | } 64 | 65 | } // namespace testing 66 | -------------------------------------------------------------------------------- /tests/testing.hpp: -------------------------------------------------------------------------------- 1 | #ifndef RGBCTL_TESTING_HPP_INCLUDED 2 | #define RGBCTL_TESTING_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #define STRINGIFY_IMPL(x) #x 9 | #define STRINGIFY(x) STRINGIFY_IMPL(x) 10 | #define TEST(fn) std::make_pair(STRINGIFY(fn), fn) 11 | #define EXPECT(cond) \ 12 | do { \ 13 | if (!(cond)) \ 14 | throw ::testing::TestFailure("Expectation not met: " STRINGIFY(cond) "\n in " __FILE__ ":" STRINGIFY(__LINE__)); \ 15 | } \ 16 | while (0) 17 | #define EXPECT_THROWS(expr) \ 18 | try { \ 19 | static_cast(expr); \ 20 | throw ::testing::TestFailure("Expectation not met: " STRINGIFY(expr) " should throw\n in " __FILE__ ":" STRINGIFY(__LINE__)); \ 21 | } \ 22 | catch (::testing::TestFailure const&) { \ 23 | throw; \ 24 | } \ 25 | catch (...) { \ 26 | } 27 | 28 | #define IGNORE(fn, reason) \ 29 | std::make_pair(STRINGIFY(fn), \ 30 | []() { throw ::testing::TestIgnored(reason); }) 31 | 32 | #define TEST_WITH_CONTEXT(fn) \ 33 | auto fn([[maybe_unused]] ::testing::TestContext const& test_context) 34 | 35 | namespace testing 36 | { 37 | 38 | struct TestContext 39 | { 40 | int argc; 41 | char const** argv; 42 | }; 43 | 44 | using TestFunction = auto(*)() -> void; 45 | using TestFunctionWithContext = auto(*)(TestContext const&) -> void; 46 | using Test = std::pair; 47 | using TestWithContext = std::pair; 48 | 49 | struct TestFailure : std::logic_error 50 | { 51 | TestFailure(char const* msg); 52 | }; 53 | 54 | struct TestIgnored : std::logic_error 55 | { 56 | TestIgnored(char const* msg); 57 | }; 58 | 59 | [[nodiscard]] auto 60 | run_with_context(int, char const**, std::initializer_list) 61 | -> int; 62 | [[nodiscard]] auto run(std::initializer_list) -> int; 63 | 64 | } // namespace testing 65 | 66 | #endif // RGBCTL_TESTING_HPP_INCLUDED 67 | -------------------------------------------------------------------------------- /tools/add-version-change: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | topdir="$(cd $(dirname $0)/..; pwd)" 4 | changesdir="${topdir}/.versioning/changes" 5 | randomstring=$(LC_ALL=C &2; exit 1; } 13 | versiontype="major" 14 | ;; 15 | -N) 16 | [ -z ${versiontype} ] || { echo "Multiple version arguments: ${arg}" >&2; exit 1; } 17 | versiontype="minor" 18 | ;; 19 | -P) 20 | [ -z ${versiontype} ] || { echo "Multiple version arguments: ${arg}" >&2; exit 1; } 21 | versiontype="patch" 22 | ;; 23 | *) 24 | echo "Unknown argument: ${arg}" >&2 25 | exit 1 26 | ;; 27 | esac 28 | shift 29 | done 30 | 31 | if [ -z ${versiontype} ]; then 32 | echo "No version type specified" >&2 33 | exit 1 34 | fi 35 | 36 | tmpchangesfile=$(mktemp "/tmp/$(basename ${0})-${$}.XXXXXX") 37 | trap "rm -rf ${tmpchangesfile}" INT TERM EXIT 38 | 39 | cat >>${tmpchangesfile} <<-EOF 40 | # Enter your change message. 41 | # Each change message should go on a separate line. 42 | # Lines beginning with '#' are comments and will be removed. 43 | # Markdown is supported. 44 | # An empty file will discard the change. 45 | 46 | EOF 47 | 48 | ${editor} ${tmpchangesfile} 49 | 50 | if [ ${?} -ne 0 ]; then 51 | echo "Received non-zero exit code. Aborting change" 52 | exit 1 53 | fi 54 | 55 | sed -i -e '/^#/d' -e '/^$/d' ${tmpchangesfile} 56 | 57 | linecount=$(wc -l "${tmpchangesfile}" | awk '{ print $1; }') 58 | 59 | if [[ "${linecount}" == "0" ]]; then 60 | echo "Empty change. Discarding" >&2 61 | exit 1 62 | fi 63 | 64 | [ -d "${changesdir}" ] || mkdir -p "${changesdir}" 65 | 66 | outputfile="${changesdir}/${randomstring}.${versiontype}.md" 67 | cp "${tmpchangesfile}" "${outputfile}" 68 | echo "Version change saved to: ${outputfile}" 69 | -------------------------------------------------------------------------------- /tools/install-helper.sh.in: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | install_prefix="@CMAKE_INSTALL_PREFIX@" 4 | project_dir="@PROJECT_SOURCE_DIR@" 5 | build_dir="@PROJECT_BINARY_DIR@" 6 | kms_bin_name="@SHADOW_CAST_KMS_BINARY_NAME@" 7 | 8 | printf \ 9 | "%s\n%s\n" \ 10 | "${install_prefix}" \ 11 | "${project_dir}" 12 | 13 | cmake \ 14 | --build "${build_dir}" \ 15 | --target install \ 16 | -- -j$(nproc) 17 | 18 | setcap \ 19 | cap_sys_admin+ep \ 20 | "${install_prefix}/bin/${kms_bin_name}" 21 | -------------------------------------------------------------------------------- /tools/make-dist: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | topdir=$(cd $(dirname $0)/..; pwd) 6 | version=$(cat ${topdir}/.versioning/current) 7 | projectname=shadow-cast 8 | 9 | cd ${topdir} 10 | 11 | [[ -d ./dist ]] && rm -rf ./dist 12 | mkdir ./dist 13 | 14 | tar \ 15 | --exclude './build' \ 16 | --exclude './dist' \ 17 | --exclude './report' \ 18 | --exclude './.private' \ 19 | --exclude './.git' \ 20 | --exclude './.github' \ 21 | --exclude './.cache' \ 22 | --transform 's;^\.;'${projectname}'-'${version}';' \ 23 | -cJf ./dist/${projectname}-source-${version}.tar.xz \ 24 | . 25 | 26 | cd ./dist 27 | sha256sum ${projectname}-source-${version}.tar.xz >${projectname}-source-${version}.sha256 28 | -------------------------------------------------------------------------------- /tools/metrics/average: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | usage() { 4 | cat >&2 <<-EOF 5 | Usage ${0} [ OPTION... ] 6 | OPTIONS 7 | -a Audio 8 | -A Audio encoder 9 | -v Video (default) 10 | -V Video encoder 11 | EOF 12 | } 13 | 14 | category=1 15 | while getopts 'aAvV' opt; do 16 | case "${opt}" in 17 | a) 18 | category=2 19 | ;; 20 | A) 21 | category=4 22 | ;; 23 | v) 24 | category=1 25 | ;; 26 | V) 27 | category=3 28 | ;; 29 | ?) 30 | usage 31 | ;; 32 | esac 33 | done 34 | 35 | shift $((OPTIND-1)) 36 | 37 | if [[ $# -lt 1 ]]; then 38 | echo "ERROR: Missing arguments" >&2 39 | usage 40 | exit 1 41 | fi 42 | 43 | <"${1}" grep -E '^'${category} | awk 'BEGIN { FS=","; count=0; sum=0; } { sum+=$4; count++; } END { print sum / count; }' 44 | -------------------------------------------------------------------------------- /tools/metrics/percentile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | usage() { 4 | cat >&2 <<-EOF 5 | Usage ${0} [ OPTION... ] 6 | OPTIONS 7 | -a Audio 8 | -A Audio Encoding 9 | -v Video (default) 10 | -V Video Encoding 11 | EOF 12 | } 13 | 14 | category=1 15 | while getopts 'aAvV' opt; do 16 | case "${opt}" in 17 | a) 18 | category=2 19 | ;; 20 | v) 21 | category=1 22 | ;; 23 | A) 24 | category=4 25 | ;; 26 | V) 27 | category=3 28 | ;; 29 | ?) 30 | usage 31 | ;; 32 | esac 33 | done 34 | 35 | shift $((OPTIND-1)) 36 | 37 | if [[ $# -lt 2 ]]; then 38 | echo "ERROR: Missing arguments" >&2 39 | usage 40 | exit 1 41 | fi 42 | 43 | #set -x 44 | 45 | <"${1}" grep -E '^'${category}'' \ 46 | | sort -k4 -t, -g \ 47 | | head -$(echo "scale=10; x=$(<"${1}" grep -E '^'${category} | wc -l)*(${2}/100); scale=0; x/1" | bc) \ 48 | | tail -n-1 49 | -------------------------------------------------------------------------------- /tools/metrics/plot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | exclude_audio=0 4 | exclude_video=0 5 | video_category=1 6 | audio_category=2 7 | 8 | while getopts 'sVAe' opt; do 9 | case "${opt}" in 10 | A) 11 | exclude_audio=1 12 | ;; 13 | e) 14 | video_category=3 15 | audio_category=4 16 | ;; 17 | s) 18 | smoothval='smooth csplines' 19 | ;; 20 | V) 21 | exclude_video=1 22 | ;; 23 | *) 24 | exit 1 25 | ;; 26 | esac 27 | done 28 | shift $((OPTIND-1)) 29 | 30 | if [[ ${#} -lt 1 ]]; then 31 | echo "Incorrect number of arguments. required" >&2 32 | exit 1 33 | fi 34 | 35 | if [[ ${exclude_audio} -ne 0 ]] && [[ ${exclude_audio} == ${exclude_video} ]]; then 36 | echo "Cannot use -A and -V together" >&2 37 | exit 1 38 | fi 39 | 40 | inputfile="${1}" 41 | yrange="4" 42 | 43 | set -x 44 | # 45 | # See https://raymii.org/s/tutorials/GNUplot_tips_for_nice_looking_charts_from_a_CSV_file.html 46 | # 47 | cat <<-EOF | gnuplot -p 48 | set terminal png size 900,600 49 | set output '${inputfile}.chart.png' 50 | set datafile separator ',' 51 | set grid 52 | set xdata time 53 | set xlabel "Time (seconds)" 54 | set ylabel "Frame Time (milliseconds)" 55 | set xtics 30 56 | set yrange [0:${yrange}] 57 | 58 | $([[ ${exclude_audio} -eq 1 ]] && echo "plot '< grep -E \"^${video_category}\" "${inputfile}" | cut -d, -f3,4' \ 59 | using (\$1/1000000000):(\$2/1000000) ${smoothval} with lines title \"Video\"") 60 | $([[ ${exclude_video} -eq 1 ]] && echo "plot '< grep -E \"^${audio_category}\" "${inputfile}" | cut -d, -f3,4' \ 61 | using (\$1/1000000000):(\$2/1000000) ${smoothval} with lines title \"Audio\"") 62 | $([[ ${exclude_audio} -eq ${exclude_video} ]] && echo "plot '< grep -E \"^${video_category}\" "${inputfile}" | cut -d, -f3,4' \ 63 | using (\$1/1000000000):(\$2/1000000) ${smoothval} with lines title \"Video\", \ 64 | '< grep -E \"^${audio_category}\" "${inputfile}" | cut -d, -f3,4' \ 65 | using (\$1/1000000000):(\$2/1000000) ${smoothval} with lines title \"Audio\"") 66 | EOF 67 | -------------------------------------------------------------------------------- /tools/next-version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | project_dir=$(cd $(dirname ${0})/..; pwd) 4 | changes_dir="${project_dir}/.versioning/changes" 5 | current_version=$(cat ${project_dir}/.versioning/current | tr -d '\n') 6 | 7 | version_list=(${current_version}) 8 | 9 | test_multiple() { 10 | [[ $(ls -1 "${changes_dir}" 2>/dev/null | grep "${1}" | wc -l) -gt 0 ]] 11 | } 12 | 13 | if test_multiple "patch" ; then 14 | version_list+=($(echo "${current_version}" | semverutil -P)) 15 | fi 16 | 17 | if test_multiple "minor" ; then 18 | version_list+=($(echo "${current_version}" | semverutil -N)) 19 | fi 20 | 21 | if test_multiple "major" ; then 22 | version_list+=($(echo "${current_version}" | semverutil -M)) 23 | fi 24 | 25 | echo ${version_list[@]} | semverutil 26 | -------------------------------------------------------------------------------- /tools/rollup-version-changes: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | topdir=$(cd $(dirname $0)/..; pwd) 4 | versiondir="${topdir}/.versioning" 5 | semverutilbin="semverutil" 6 | 7 | nextversionupdate=$(find "${versiondir}/changes" -type f -name '*.patch.*' \ 8 | | (grep . >/dev/null && echo "PATCH")) 9 | nextversionupdate=$(find "${versiondir}/changes" -type f -name '*.minor.*' \ 10 | | (grep . >/dev/null && echo "MINOR" || echo "${nextversionupdate}")) 11 | nextversionupdate=$(find "${versiondir}/changes" -type f -name '*.major.*' \ 12 | | (grep . >/dev/null && echo "MAJOR" || echo "${nextversionupdate}")) 13 | 14 | case ${nextversionupdate} in 15 | PATCH) 16 | versionbumpparam="-P" 17 | ;; 18 | MINOR) 19 | versionbumpparam="-N" 20 | ;; 21 | MAJOR) 22 | versionbumpparam="-M" 23 | ;; 24 | esac 25 | 26 | if [ -z $versionbumpparam ]; then 27 | echo 'No changes' >&2 28 | exit 1 29 | fi 30 | 31 | versionbufferfile=$(mktemp "/tmp/$(basename $0)-${$}.XXXXXX") 32 | changesbufferfile=$(mktemp "/tmp/$(basename $0)-changes-${$}.XXXXXX") 33 | trap 'rm -rf ${versionbufferfile} ${changesbufferfile}' INT TERM EXIT 34 | 35 | <"${versiondir}/current" ${semverutilbin} "${versionbumpparam}" >"${versionbufferfile}" || { 36 | echo "Couldn't change ${nextversionupdate} version" >&2 37 | exit 1 38 | } 39 | 40 | if [ -x "${versiondir}/version-update" ]; then 41 | while read change; do 42 | [ -n "{change}" ] \ 43 | && printf "MAJOR %s\n" "$(echo "${change}" | sed 's;^- ;;')" \ 44 | >>"${changesbufferfile}" 45 | done < <(cat "${versiondir}/changes/"*.major.* 2>/dev/null) || true 46 | 47 | while read change; do 48 | [ -n "{change}" ] \ 49 | && printf "MINOR %s\n" "$(echo "${change}" | sed 's;^- ;;')" \ 50 | >>"${changesbufferfile}" 51 | done < <(cat "${versiondir}/changes/"*.minor.* 2>/dev/null) || true 52 | 53 | while read change; do 54 | [ -n "{change}" ] \ 55 | && printf "PATCH %s\n" "$(echo "${change}" | sed 's;^- ;;')" \ 56 | >>"${changesbufferfile}" 57 | done < <(cat "${versiondir}/changes/"*.patch.* 2>/dev/null) || true 58 | 59 | export NEXT_VERSION=$(cat "${versionbufferfile}") 60 | export CHANGES_FILE="${changesbufferfile}" 61 | if ! (cd "${topdir}"; "${versiondir}/version-update"); then 62 | echo "Couldn't change version: version-update returned $?" >&2 63 | exit 1 64 | fi 65 | fi 66 | 67 | cp "${versionbufferfile}" "${versiondir}/current" 68 | rm -rf "${versiondir}/changes/"* 69 | --------------------------------------------------------------------------------