├── docs ├── requirements.txt ├── Doxyfile ├── index.rst ├── cpp │ ├── installation.rst │ ├── reference.rst │ ├── quick_start.rst │ └── advanced.rst ├── Makefile ├── make.bat ├── python │ └── basics.rst └── conf.py ├── python ├── src │ ├── dxcam_cpp │ │ └── __init__.py │ ├── util │ │ ├── string_cvt.h │ │ ├── numpy.h │ │ ├── string_cvt.cpp │ │ └── numpy.cpp │ ├── py_dxcam.h │ ├── py_FrameBuffer.h │ ├── py_FrameBuffer.cpp │ ├── py_DXCamera.h │ ├── py_bindings.cpp │ ├── py_dxcam.cpp │ └── py_DXCamera.cpp ├── README.md ├── examples │ ├── single_screenshot.py │ ├── get_frame_buffer.py │ └── continuous_grabbing.py ├── benchmarks │ └── max_fps.py └── CMakeLists.txt ├── .clang-format ├── src ├── core │ ├── Region.cpp │ ├── OutputMetadata.cpp │ ├── Processor.h │ ├── OutputMetadata.h │ ├── Device.h │ ├── Output.h │ ├── Duplicator.h │ ├── StageSurface.h │ ├── Device.cpp │ ├── Output.cpp │ ├── StageSurface.cpp │ ├── Processor.cpp │ └── Duplicator.cpp ├── util │ ├── io.h │ ├── HighResTimer.h │ ├── HighResTimer.cpp │ └── io.cpp ├── dxcam.cpp ├── DXFactory.h ├── DXFactory.cpp └── DXCamera.cpp ├── vcpkg.json ├── .gitignore ├── cmake └── DXCamConfig.cmake.in ├── vcpkg-configuration.json ├── benchmarks ├── CMakeLists.txt ├── max_fps.cpp └── target_fps.cpp ├── .readthedocs.yaml ├── include └── dxcam │ ├── core │ ├── DeviceInfo.h │ ├── OutputInfo.h │ └── Region.h │ ├── dxcam.h │ └── DXCamera.h ├── examples ├── CMakeLists.txt ├── video_mode.cpp └── screenshot.cpp ├── LICENSE ├── pyproject.toml ├── CMakePresets.json ├── README.md ├── CMakeLists.txt └── .github └── workflows └── workflow.yml /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | breathe 3 | furo -------------------------------------------------------------------------------- /python/src/dxcam_cpp/__init__.py: -------------------------------------------------------------------------------- 1 | from .dxcam_cpp import * 2 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | IndentWidth: 4 3 | UseTab: Never 4 | AllowShortBlocksOnASingleLine: Always 5 | -------------------------------------------------------------------------------- /python/src/util/string_cvt.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_PYTHON_UTIL_STRING_CVT_H 2 | #define DXCAM_CPP_PYTHON_UTIL_STRING_CVT_H 3 | 4 | #include 5 | 6 | std::string wstring_to_string(const std::wstring &str); 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /src/core/Region.cpp: -------------------------------------------------------------------------------- 1 | #include "core/Region.h" 2 | 3 | namespace DXCam { 4 | 5 | int Region::get_width() const { return right - left; } 6 | 7 | int Region::get_height() const { return bottom - top; } 8 | 9 | } // namespace DXCam -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "opencv4", 5 | "default-features": false, 6 | "features": [ 7 | "highgui", 8 | "win32ui" 9 | ] 10 | }, 11 | "pybind11" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /python/src/util/numpy.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_PYTHON_UTIL_NUMPY_H 2 | #define DXCAM_CPP_PYTHON_UTIL_NUMPY_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace py = pybind11; 9 | 10 | py::array_t numpy_array_from(const cv::Mat &&mat); 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | # Distribution / packaging 4 | .Python 5 | build/ 6 | develop-eggs/ 7 | dist/ 8 | downloads/ 9 | eggs/ 10 | .eggs/ 11 | lib/ 12 | lib64/ 13 | parts/ 14 | sdist/ 15 | var/ 16 | wheels/ 17 | share/python-wheels/ 18 | *.egg-info/ 19 | .installed.cfg 20 | *.egg 21 | MANIFEST 22 | 23 | docs/_build/ 24 | docs/_static/ 25 | docs/_templates/ 26 | -------------------------------------------------------------------------------- /src/util/io.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_IO_H 2 | #define DXCAM_CPP_IO_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "core/OutputMetadata.h" 9 | 10 | namespace DXCam { 11 | 12 | std::vector enum_dxgi_adapters(); 13 | 14 | OutputMetadata get_output_metadata(); 15 | 16 | } // namespace DXCam 17 | 18 | #endif // DXCAM_CPP_IO_H 19 | -------------------------------------------------------------------------------- /src/util/HighResTimer.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_HIGHRESTIMER_H 2 | #define DXCAM_CPP_HIGHRESTIMER_H 3 | 4 | #include 5 | 6 | class HighResTimer { 7 | public: 8 | explicit HighResTimer(int period_ms); 9 | void wait() const; 10 | void cancel() const; 11 | 12 | private: 13 | HANDLE handle_; 14 | }; 15 | 16 | 17 | #endif // DXCAM_CPP_HIGHRESTIMER_H 18 | -------------------------------------------------------------------------------- /cmake/DXCamConfig.cmake.in: -------------------------------------------------------------------------------- 1 | @PACKAGE_INIT@ 2 | 3 | include(CMakeFindDependencyMacro) 4 | find_dependency(OpenCV REQUIRED) 5 | 6 | include("${CMAKE_CURRENT_LIST_DIR}/DXCamTargets.cmake") 7 | 8 | set_and_check(DXCam_INCLUDE_DIRS "@PACKAGE_INCLUDE_INSTALL_DIR@") 9 | set(DXCam_LIBS DXCam::DXCam) 10 | 11 | message(STATUS "Found DXCam: ${DXCam_DIR} (found version ${DXCam_VERSION})") -------------------------------------------------------------------------------- /src/core/OutputMetadata.cpp: -------------------------------------------------------------------------------- 1 | #include "core/OutputMetadata.h" 2 | 3 | namespace DXCam { 4 | 5 | AdapterMetadata OutputMetadata::get( 6 | const std::wstring &adapter_device_name) const { 7 | for (const auto &adapter : adapters) { 8 | if (adapter.device_name == adapter_device_name) { return adapter; } 9 | } 10 | return {}; 11 | } 12 | 13 | } // namespace DXCam 14 | -------------------------------------------------------------------------------- /docs/Doxyfile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME = DXCam-CPP 2 | INPUT = ../include/dxcam/ 3 | RECURSIVE = YES 4 | 5 | OUTPUT_DIRECTORY = _build 6 | 7 | GENERATE_HTML = NO 8 | GENERATE_LATEX = NO 9 | GENERATE_XML = YES 10 | XML_PROGRAMLISTING = YES 11 | 12 | MACRO_EXPANSION = YES 13 | EXPAND_ONLY_PREDEF = YES 14 | PREDEFINED = DXCAM_EXPORT:= -------------------------------------------------------------------------------- /vcpkg-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "default-registry": { 3 | "kind": "git", 4 | "baseline": "d504de05dcd7b55df34976be1c824324ec6bca2b", 5 | "repository": "https://github.com/microsoft/vcpkg" 6 | }, 7 | "registries": [ 8 | { 9 | "kind": "artifact", 10 | "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", 11 | "name": "microsoft" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /benchmarks/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | message(STATUS "Configuring benchmark") 2 | 3 | # Benchmark: max_fps 4 | add_executable(benchmark_max_fps max_fps.cpp) 5 | target_link_libraries(benchmark_max_fps DXCam) 6 | add_dependencies(benchmark_max_fps DXCam) 7 | 8 | # Benchmark: target_fps 9 | add_executable(benchmark_target_fps target_fps.cpp) 10 | target_link_libraries(benchmark_target_fps DXCam) 11 | add_dependencies(benchmark_target_fps DXCam) 12 | 13 | message(STATUS "Configuring benchmark - done") -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # DXCam-CPP 2 | 3 | A high performance screen capturing library for Windows 4 | rewriting [DXcam](https://github.com/ra1nty/DXcam) in C++. 5 | 6 | ## Usage 7 | 8 | ```shell 9 | pip install dxcam-cpp 10 | ``` 11 | 12 | The interface of the Python module of DXCam_CPP is designed to be **fully 13 | compatible** with the original Python DXcam as an alternative. 14 | See [the documentation of DXcam](https://github.com/ra1nty/DXcam?tab=readme-ov-file#usage) 15 | for more details. -------------------------------------------------------------------------------- /python/src/util/string_cvt.cpp: -------------------------------------------------------------------------------- 1 | #include "string_cvt.h" 2 | 3 | #include 4 | 5 | std::string wstring_to_string(const std::wstring &str) { 6 | const int len = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), -1, nullptr, 0, 7 | nullptr, nullptr); 8 | const auto buf = new char[len]; 9 | WideCharToMultiByte(CP_UTF8, 0, str.c_str(), -1, buf, len, nullptr, 10 | nullptr); 11 | std::string ret(buf); 12 | delete[] buf; 13 | return ret; 14 | } -------------------------------------------------------------------------------- /src/core/Processor.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_PROCESSOR_H 2 | #define DXCAM_CPP_PROCESSOR_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | #include "core/Region.h" 9 | 10 | namespace DXCam { 11 | 12 | class Processor { 13 | public: 14 | Processor() = default; 15 | 16 | static cv::Mat process(const DXGI_MAPPED_RECT &rect, int width, int height, 17 | const Region ®ion, int rotation_angle); 18 | }; 19 | 20 | } // namespace DXCam 21 | 22 | #endif // DXCAM_CPP_PROCESSOR_H 23 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | DXCam-CPP Documentation 2 | ####################### 3 | 4 | DXCam-CPP is a high performance screen capturing library for Windows, porting 5 | `DXcam `_ to C++. 6 | 7 | Table of Contents 8 | ================= 9 | 10 | .. toctree:: 11 | :caption: Usage as a C++ Library 12 | :maxdepth: 2 13 | 14 | cpp/installation 15 | cpp/quick_start 16 | cpp/advanced 17 | cpp/reference 18 | 19 | .. toctree:: 20 | :caption: Usage as a Python Module 21 | :maxdepth: 2 22 | 23 | python/basics 24 | -------------------------------------------------------------------------------- /python/src/py_dxcam.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_PYTHON_PY_DXCAM_H 2 | #define DXCAM_CPP_PYTHON_PY_DXCAM_H 3 | 4 | #include 5 | 6 | #include "py_DXCamera.h" 7 | 8 | namespace py = pybind11; 9 | 10 | std::string device_info(); 11 | std::string output_info(); 12 | 13 | std::shared_ptr create(int device_idx, std::optional output_idx, 14 | const std::optional ®ion, 15 | const std::string &output_color, 16 | size_t max_buffer_len); 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version, and other tools you might need 8 | build: 9 | os: ubuntu-24.04 10 | tools: 11 | python: "3.13" 12 | 13 | # Build documentation in the "docs/" directory with Sphinx 14 | sphinx: 15 | configuration: docs/conf.py 16 | 17 | # Optionally, but recommended, 18 | # declare the Python requirements required to build your documentation 19 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | python: 21 | install: 22 | - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /python/src/py_FrameBuffer.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_PYTHON_PY_FRAMEBUFFER_H 2 | #define DXCAM_CPP_PYTHON_PY_FRAMEBUFFER_H 3 | 4 | #include 5 | #include 6 | 7 | #include "dxcam/DXCamera.h" 8 | 9 | namespace py = pybind11; 10 | 11 | class FrameBuffer { 12 | public: 13 | explicit FrameBuffer(const std::shared_ptr &camera); 14 | 15 | std::vector> enter(); 16 | void exit(const py::object &type, const py::object &value, 17 | const py::object &traceback) const; 18 | 19 | private: 20 | std::shared_ptr camera_; 21 | std::mutex *mutex_ = nullptr; 22 | }; 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /docs/cpp/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ############ 3 | 4 | Include with vcpkg 5 | ================== 6 | DXCam-CPP is available on `vcpkg `_. 7 | 8 | In classic mode, run the following vcpkg command to install: 9 | 10 | .. code-block:: shell 11 | 12 | vcpkg install dxcam-cpp 13 | 14 | In manifest mode, run the following vcpkg command in your project directory to 15 | add the port: 16 | 17 | .. code-block:: shell 18 | 19 | vcpkg add port dxcam-cpp 20 | 21 | Include with prebuilt binaries 22 | ============================== 23 | 24 | You may download the prebuilt binaries from the 25 | `Releases `_ page. 26 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /python/src/util/numpy.cpp: -------------------------------------------------------------------------------- 1 | #include "numpy.h" 2 | 3 | py::array_t numpy_array_from(const cv::Mat &&mat) { 4 | const auto mat_ = new cv::Mat(std::move(mat)); // ? 5 | 6 | const py::capsule capsule( 7 | mat_, [](void *data) { delete static_cast(data); }); 8 | 9 | if (mat_->isContinuous()) { 10 | return py::array_t({mat_->rows, mat_->cols, mat_->channels()}, 11 | mat_->data, capsule); 12 | } else { 13 | return py::array_t( 14 | {mat_->rows, mat_->cols, mat_->channels()}, 15 | {mat_->step[0], mat_->step[1], static_cast(1)}, mat_->data, 16 | capsule); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /include/dxcam/core/DeviceInfo.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_DEVICEINFO_H 2 | #define DXCAM_CPP_DEVICEINFO_H 3 | 4 | #include 5 | #include 6 | 7 | namespace DXCam { 8 | 9 | /** 10 | * @brief The information of a device, i.e. graphics card. 11 | */ 12 | struct DeviceInfo { 13 | /** 14 | * @brief The description of the device. 15 | */ 16 | [[maybe_unused]] std::wstring description; 17 | 18 | /** 19 | * @brief The size of the VRAM in MiB. 20 | */ 21 | [[maybe_unused]] SIZE_T vram_size; 22 | 23 | /** 24 | * @brief The vendor ID of the device. 25 | */ 26 | [[maybe_unused]] UINT vendor_id; 27 | }; 28 | 29 | } // namespace DXCam 30 | 31 | #endif // DXCAM_CPP_DEVICEINFO_H 32 | -------------------------------------------------------------------------------- /docs/cpp/reference.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ######### 3 | 4 | Functions 5 | ========= 6 | 7 | .. doxygenfunction:: DXCam::create(int device_idx = 0, int output_idx = -1, size_t max_buffer_len = 64) 8 | 9 | .. doxygenfunction:: DXCam::create(const Region ®ion, int device_idx = 0, int output_idx = -1, size_t max_buffer_len = 64) 10 | 11 | .. doxygenfunction:: DXCam::get_devices_info 12 | 13 | .. doxygenfunction:: DXCam::get_outputs_info 14 | 15 | Structs 16 | ======= 17 | 18 | .. doxygenstruct:: DXCam::DeviceInfo 19 | :members: 20 | 21 | .. doxygenstruct:: DXCam::OutputInfo 22 | :members: 23 | 24 | Classes 25 | ======= 26 | 27 | .. doxygenclass:: DXCam::DXCamera 28 | :members: 29 | 30 | .. doxygenclass:: DXCam::Region 31 | :members: 32 | -------------------------------------------------------------------------------- /src/core/OutputMetadata.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_OUTPUTMETADATA_H 2 | #define DXCAM_CPP_OUTPUTMETADATA_H 3 | 4 | #include 5 | #include 6 | 7 | namespace DXCam { 8 | 9 | struct DisplayMetadata { 10 | std::wstring device_name; 11 | std::wstring device_string; 12 | }; 13 | 14 | struct AdapterMetadata { 15 | std::wstring device_name; 16 | std::wstring device_string; 17 | bool is_primary; 18 | std::vector displays; 19 | }; 20 | 21 | class OutputMetadata { 22 | public: 23 | [[nodiscard]] AdapterMetadata get( 24 | const std::wstring &adapter_device_name) const; 25 | 26 | std::vector adapters; 27 | }; 28 | 29 | } // namespace DXCam 30 | 31 | #endif // DXCAM_CPP_OUTPUTMETADATA_H 32 | -------------------------------------------------------------------------------- /src/core/Device.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_DEVICE_H 2 | #define DXCAM_CPP_DEVICE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "../../include/dxcam/core/DeviceInfo.h" 11 | 12 | namespace DXCam { 13 | 14 | class Device { 15 | public: 16 | explicit Device(IDXGIAdapter1 *adapter); 17 | 18 | [[nodiscard]] DeviceInfo get_info() const; 19 | [[nodiscard]] std::vector enum_outputs() const; 20 | 21 | IDXGIAdapter1 *adapter; 22 | ID3D11Device *device = nullptr; 23 | ID3D11DeviceContext *context = nullptr; 24 | ID3D11DeviceContext *im_context = nullptr; 25 | DXGI_ADAPTER_DESC1 desc; 26 | }; 27 | 28 | } // namespace DXCam 29 | 30 | #endif // DXCAM_CPP_DEVICE_H 31 | -------------------------------------------------------------------------------- /python/examples/single_screenshot.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | import cv2 4 | # import dxcam 5 | import dxcam_cpp as dxcam 6 | 7 | if __name__ == '__main__': 8 | print(dxcam.device_info()) 9 | print(dxcam.output_info()) 10 | 11 | camera = dxcam.create(region=(0, 0, 1920, 1080), 12 | output_color='BGR') # BGR for OpenCV 13 | print(f'{camera.width=}') 14 | print(f'{camera.height=}') 15 | print(f'{camera.channel_size=}') 16 | print(f'{camera.rotation_angle=}') 17 | print(f'{camera.region=}') 18 | print(f'{camera.max_buffer_len=}') 19 | print(f'{camera.is_capturing=}') 20 | 21 | sleep(0.1) 22 | 23 | # Grab a single frame 24 | image = camera.grab() 25 | cv2.imshow("Single Frame Grabbing", image) 26 | cv2.waitKey() 27 | -------------------------------------------------------------------------------- /src/util/HighResTimer.cpp: -------------------------------------------------------------------------------- 1 | #include "HighResTimer.h" 2 | 3 | #include 4 | 5 | HighResTimer::HighResTimer(const int period_ms) 6 | : handle_(CreateWaitableTimerExW(nullptr, nullptr, 7 | CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, 8 | TIMER_ALL_ACCESS)) { 9 | constexpr LARGE_INTEGER due_time{0, 0}; // init to zero 10 | const bool res = SetWaitableTimer(handle_, &due_time, period_ms, nullptr, 11 | nullptr, false); 12 | assert(res); 13 | } 14 | 15 | void HighResTimer::wait() const { 16 | const auto res = WaitForSingleObject(handle_, INFINITE); 17 | assert(res == 0); 18 | } 19 | 20 | void HighResTimer::cancel() const { 21 | const bool res = CancelWaitableTimer(handle_); 22 | assert(res); 23 | } 24 | -------------------------------------------------------------------------------- /python/benchmarks/max_fps.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import numpy as np 4 | 5 | REPEATS = 100 6 | 7 | 8 | def benchmark(title: str, dxcam): 9 | camera = dxcam.create(region=(0, 0, 1920, 1080)) 10 | durations = [] 11 | i = 0 12 | while i < REPEATS: 13 | begin_time = time.perf_counter_ns() 14 | frame = camera.grab() 15 | end_time = time.perf_counter_ns() 16 | if frame is not None: 17 | durations.append((end_time - begin_time) / (10 ** 6)) 18 | i += 1 19 | 20 | durations = np.array(durations) 21 | mean = np.mean(durations) 22 | std = np.std(durations) 23 | print(f"{title}: {mean:.3f} ms/frame ± {std:.3f} ms/frame") 24 | 25 | 26 | if __name__ == '__main__': 27 | # import dxcam 28 | import dxcam_cpp as dxcam 29 | 30 | benchmark("DXCam Benchmark", dxcam) 31 | -------------------------------------------------------------------------------- /python/examples/get_frame_buffer.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | import dxcam_cpp as dxcam 4 | 5 | if __name__ == '__main__': 6 | print(dxcam.device_info()) 7 | print(dxcam.output_info()) 8 | 9 | camera = dxcam.create(region=(0, 0, 1920, 1080), 10 | output_color='BGR', # BGR for OpenCV 11 | max_buffer_len=16) 12 | print(f'{camera.width=}') 13 | print(f'{camera.height=}') 14 | print(f'{camera.channel_size=}') 15 | print(f'{camera.rotation_angle=}') 16 | print(f'{camera.region=}') 17 | print(f'{camera.max_buffer_len=}') 18 | print(f'{camera.is_capturing=}') 19 | 20 | camera.start() 21 | 22 | sleep(1) 23 | 24 | with camera.frame_buffer() as buffer: 25 | print("Buffer Length:", len(buffer)) 26 | print(buffer) 27 | 28 | camera.stop() 29 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | message(STATUS "Configuring examples") 2 | 3 | find_package(OpenCV REQUIRED core highgui) 4 | 5 | add_executable(example_screenshot screenshot.cpp) 6 | target_include_directories(example_screenshot PRIVATE ../include) 7 | target_link_libraries(example_screenshot DXCam dxgi d3d11 ${OpenCV_LIBS}) 8 | install(TARGETS example_screenshot DESTINATION bin 9 | RUNTIME DESTINATION bin 10 | ) 11 | add_dependencies(example_screenshot DXCam) 12 | 13 | add_executable(example_video_mode video_mode.cpp) 14 | target_include_directories(example_video_mode PRIVATE ../include) 15 | target_link_libraries(example_video_mode DXCam dxgi d3d11 ${OpenCV_LIBS}) 16 | install(TARGETS example_video_mode DESTINATION bin 17 | RUNTIME DESTINATION bin 18 | ) 19 | add_dependencies(example_video_mode DXCam) 20 | 21 | message(STATUS "Configuring examples - done") 22 | -------------------------------------------------------------------------------- /src/core/Output.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_OUTPUT_H 2 | #define DXCAM_CPP_OUTPUT_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "../../include/dxcam/core/OutputInfo.h" 10 | 11 | namespace DXCam { 12 | 13 | class Output { 14 | public: 15 | explicit Output(IDXGIOutput1 *output); 16 | 17 | [[nodiscard]] OutputInfo get_info() const; 18 | [[nodiscard]] std::wstring get_device_name() const; 19 | void get_resolution(LONG *width, LONG *height) const; 20 | void get_surface_size(LONG *width, LONG *height) const; 21 | [[nodiscard]] int get_rotation_angle() const; 22 | 23 | void update_desc(); 24 | 25 | IDXGIOutput1 *output; 26 | 27 | private: 28 | constexpr static int ROTATION_MAPPING[] = {0, 0, 90, 180, 270}; 29 | 30 | DXGI_OUTPUT_DESC desc_; 31 | }; 32 | 33 | } // namespace DXCam 34 | 35 | #endif // DXCAM_CPP_OUTPUT_H 36 | -------------------------------------------------------------------------------- /src/dxcam.cpp: -------------------------------------------------------------------------------- 1 | #include "dxcam.h" 2 | 3 | #include "DXFactory.h" 4 | 5 | namespace DXCam { 6 | 7 | static auto factory = DXFactory(); 8 | 9 | std::shared_ptr create(const int device_idx, const int output_idx, 10 | const size_t max_buffer_len) { 11 | return factory.create(device_idx, output_idx, max_buffer_len); 12 | } 13 | 14 | std::shared_ptr create(const Region ®ion, const int device_idx, 15 | const int output_idx, 16 | const size_t max_buffer_len) { 17 | return factory.create(region, device_idx, output_idx, max_buffer_len); 18 | } 19 | 20 | std::vector get_devices_info() { 21 | return factory.get_devices_info(); 22 | } 23 | 24 | std::vector> get_outputs_info() { 25 | return factory.get_outputs_info(); 26 | } 27 | 28 | } // namespace DXCam -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /include/dxcam/core/OutputInfo.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_OUTPUTINFO_H 2 | #define DXCAM_CPP_OUTPUTINFO_H 3 | 4 | #include 5 | 6 | namespace DXCam { 7 | 8 | /** 9 | * @brief The information of an output, i.e. screen. 10 | */ 11 | struct OutputInfo { 12 | /** 13 | * @brief The name of the device. 14 | */ 15 | [[maybe_unused]] std::wstring device_name; 16 | 17 | /** 18 | * @brief The width of the output. 19 | */ 20 | [[maybe_unused]] LONG width; 21 | 22 | /** 23 | * @brief The height of the output. 24 | */ 25 | [[maybe_unused]] LONG height; 26 | 27 | /** 28 | * @brief The rotation angle of the output. 29 | */ 30 | [[maybe_unused]] int rotation_angle; 31 | 32 | /** 33 | * @brief Indicates if the output is the primary display. 34 | */ 35 | [[maybe_unused]] bool is_primary; 36 | }; 37 | 38 | } // namespace DXCam 39 | 40 | #endif // DXCAM_CPP_OUTPUTINFO_H 41 | -------------------------------------------------------------------------------- /python/examples/continuous_grabbing.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | 3 | # import dxcam 4 | import dxcam_cpp as dxcam 5 | 6 | if __name__ == '__main__': 7 | print(dxcam.device_info()) 8 | print(dxcam.output_info()) 9 | 10 | camera = dxcam.create(region=(0, 0, 1920, 1080), 11 | output_color='BGR') # BGR for OpenCV 12 | print(f'{camera.width=}') 13 | print(f'{camera.height=}') 14 | print(f'{camera.channel_size=}') 15 | print(f'{camera.rotation_angle=}') 16 | print(f'{camera.region=}') 17 | print(f'{camera.max_buffer_len=}') 18 | print(f'{camera.is_capturing=}') 19 | 20 | # Grab frames continuously 21 | try: 22 | camera.start() 23 | while True: 24 | image = camera.get_latest_frame() 25 | cv2.imshow("Continuous Frame Grabbing", image) 26 | cv2.waitKey() 27 | except KeyboardInterrupt: 28 | pass 29 | finally: 30 | camera.stop() 31 | -------------------------------------------------------------------------------- /src/core/Duplicator.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_DUPLICATOR_H 2 | #define DXCAM_CPP_DUPLICATOR_H 3 | 4 | #include "Device.h" 5 | #include "Output.h" 6 | 7 | namespace DXCam { 8 | 9 | class Duplicator { 10 | public: 11 | Duplicator(const Output *output, const Device *device); 12 | ~Duplicator(); 13 | 14 | // Disallow copy and move 15 | Duplicator(const Duplicator &) = delete; 16 | Duplicator &operator=(const Duplicator &) = delete; 17 | Duplicator(Duplicator &&other) = delete; 18 | Duplicator &operator=(Duplicator &&other) = delete; 19 | 20 | void rebuild(const Output *output, const Device *device); 21 | 22 | bool update_frame(); 23 | void release_frame() const; 24 | 25 | ID3D11Texture2D *texture = nullptr; 26 | bool updated = false; 27 | 28 | private: 29 | void create(const Output *output, const Device *device); 30 | void release(); 31 | 32 | IDXGIOutputDuplication *duplicator_ = nullptr; 33 | }; 34 | 35 | } // namespace DXCam 36 | 37 | #endif // DXCAM_CPP_DUPLICATOR_H 38 | -------------------------------------------------------------------------------- /python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | message(STATUS "Configuring DXCam-CPP-Python") 2 | 3 | find_package(Python COMPONENTS Interpreter Development) 4 | find_package(pybind11 CONFIG REQUIRED) 5 | 6 | pybind11_add_module(dxcam_cpp MODULE 7 | src/util/numpy.cpp 8 | src/util/string_cvt.cpp 9 | src/py_bindings.cpp 10 | src/py_dxcam.cpp 11 | src/py_DXCamera.cpp 12 | src/py_FrameBuffer.cpp 13 | ) 14 | target_link_libraries(dxcam_cpp PRIVATE DXCam) 15 | add_dependencies(dxcam_cpp DXCam) 16 | 17 | if (INSTALL_PYTHON_ONLY) 18 | set(PYTHON_INSTALL_DIR dxcam_cpp) 19 | else () 20 | set(PYTHON_INSTALL_DIR python/dxcam_cpp) 21 | endif () 22 | 23 | # Collect DLLs 24 | install(TARGETS dxcam_cpp 25 | RUNTIME DESTINATION ${PYTHON_INSTALL_DIR} 26 | LIBRARY DESTINATION ${PYTHON_INSTALL_DIR} 27 | ARCHIVE DESTINATION ${PYTHON_INSTALL_DIR} 28 | ) 29 | install(FILES $ 30 | DESTINATION ${PYTHON_INSTALL_DIR} 31 | ) 32 | 33 | message(STATUS "Configuring DXCam-CPP-Python - done") 34 | -------------------------------------------------------------------------------- /include/dxcam/core/Region.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_REGION_H 2 | #define DXCAM_CPP_REGION_H 3 | 4 | namespace DXCam { 5 | 6 | /** 7 | * @brief Represents a rectangular region. 8 | */ 9 | class Region { 10 | public: 11 | /** 12 | * @brief The coordinate of the left boundary of the region. 13 | */ 14 | int left; 15 | /** 16 | * @brief The coordinate of the top boundary of the region. 17 | */ 18 | int top; 19 | /** 20 | * @brief The coordinate of the right boundary of the region. 21 | */ 22 | int right; 23 | /** 24 | * @brief The coordinate of the bottom boundary of the region. 25 | */ 26 | int bottom; 27 | 28 | /** 29 | * @brief Get the width of the region. 30 | * @return The width of the region. 31 | */ 32 | [[nodiscard]] int get_width() const; 33 | 34 | /** 35 | * @brief Get the height of the region. 36 | * @return The height of the region. 37 | */ 38 | [[nodiscard]] int get_height() const; 39 | }; 40 | 41 | } // namespace DXCam 42 | 43 | #endif // DXCAM_CPP_REGION_H 44 | -------------------------------------------------------------------------------- /python/src/py_FrameBuffer.cpp: -------------------------------------------------------------------------------- 1 | #include "py_FrameBuffer.h" 2 | 3 | #include "util/numpy.h" 4 | 5 | FrameBuffer::FrameBuffer(const std::shared_ptr &camera) 6 | : camera_(camera) {} 7 | 8 | std::vector> FrameBuffer::enter() { 9 | const cv::Mat *const *frame_buffer; 10 | const std::atomic_int *head, *tail; 11 | const size_t *len; 12 | 13 | camera_->get_frame_buffer(&frame_buffer, nullptr, &len, &head, &tail, 14 | nullptr, &mutex_); 15 | 16 | mutex_->lock(); 17 | 18 | std::vector> frames; 19 | for (size_t i = *head; i != *tail; i = (i + 1) % *len) { 20 | cv::Mat frame = (*frame_buffer)[i]; 21 | frames.push_back(numpy_array_from(std::move(frame))); 22 | } 23 | 24 | return frames; 25 | } 26 | 27 | void FrameBuffer::exit([[maybe_unused]] const py::object &type, 28 | [[maybe_unused]] const py::object &value, 29 | [[maybe_unused]] const py::object &traceback) const { 30 | if (mutex_ != nullptr) { mutex_->unlock(); } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Rain 4 | Copyright (c) 2023 Fidel Yin 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /docs/python/basics.rst: -------------------------------------------------------------------------------- 1 | Basics 2 | ###### 3 | 4 | Installation 5 | ============ 6 | 7 | DXCam_CPP is available on `PyPI `_. You can install it via pip: 8 | 9 | .. code-block:: shell 10 | 11 | pip install dxcam_cpp 12 | 13 | Usage 14 | ===== 15 | 16 | The interface of the Python module of DXCam-CPP is designed to be **fully 17 | compatible** with the original Python DXcam as an alternative. 18 | 19 | Therefore, you can simply import this module and use it as DXcam: 20 | 21 | .. code-block:: python 22 | 23 | import dxcam_cpp as dxcam 24 | 25 | See `the 26 | documentation of DXcam 27 | `_ for more details. 28 | 29 | .. note:: 30 | 31 | Consuming frames from buffer 32 | ============================ 33 | 34 | DXCam_CPP provides access to the frame buffer via the ``frame_buffer()`` method. 35 | With the context manager, you can obtain a list of frames. 36 | 37 | .. code-block:: python 38 | 39 | camera = dxcam.create(max_buffer_len=16) 40 | 41 | camera.start() 42 | with camera.frame_buffer() as buffer: 43 | # Do something with the buffer 44 | pass 45 | camera.stop() 46 | -------------------------------------------------------------------------------- /src/core/StageSurface.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_STAGESURFACE_H 2 | #define DXCAM_CPP_STAGESURFACE_H 3 | 4 | #include 5 | #include 6 | 7 | #include "Device.h" 8 | #include "Output.h" 9 | 10 | namespace DXCam { 11 | 12 | class StageSurface { 13 | public: 14 | StageSurface(const Output *output, const Device *device); 15 | ~StageSurface(); 16 | 17 | // Disallow copy and move 18 | StageSurface(const StageSurface &) = delete; 19 | StageSurface &operator=(const StageSurface &) = delete; 20 | StageSurface(StageSurface &&other) = delete; 21 | StageSurface &operator=(StageSurface &&other) = delete; 22 | 23 | void rebuild(const Output *output, const Device *device); 24 | 25 | [[nodiscard]] DXGI_MAPPED_RECT map() const; 26 | void unmap() const; 27 | 28 | ID3D11Texture2D *texture = nullptr; 29 | 30 | private: 31 | void create(const Output *output, const Device *device); 32 | void release(); 33 | 34 | LONG width_ = 0; 35 | LONG height_ = 0; 36 | DXGI_FORMAT dxgi_format_ = DXGI_FORMAT_B8G8R8A8_UNORM; 37 | IDXGISurface *surface_ = nullptr; 38 | }; 39 | 40 | } // namespace DXCam 41 | 42 | #endif // DXCAM_CPP_STAGESURFACE_H 43 | -------------------------------------------------------------------------------- /examples/video_mode.cpp: -------------------------------------------------------------------------------- 1 | #include "dxcam/dxcam.h" 2 | #include "opencv2/opencv.hpp" 3 | 4 | int main() { 5 | const auto devices_info = DXCam::get_devices_info(); 6 | const auto outputs_info = DXCam::get_outputs_info(); 7 | 8 | for (size_t i = 0; i < devices_info.size(); i++) { 9 | const auto &[description, vram_size, vendor_id] = devices_info[i]; 10 | printf("Device [%llu]: %ls VRam:%llu VendorId:%u\n", i, 11 | description.c_str(), vram_size, vendor_id); 12 | for (size_t j = 0; j < outputs_info[i].size(); j++) { 13 | const auto &[device_name, width, height, rotation_angle, 14 | is_primary] = outputs_info[i][j]; 15 | printf("\tOutput [%llu]: %ls Res:%ldx%ld Rot:%d IsPrimary:%d\n", j, 16 | device_name.c_str(), width, height, rotation_angle, 17 | is_primary); 18 | } 19 | } 20 | 21 | const auto camera = DXCam::create(); 22 | camera->start(60, true); 23 | 24 | while (true) { 25 | auto frame = camera->get_latest_frame(); 26 | cv::imshow("frame", frame); 27 | if (cv::waitKey(0) == 27) { // ESC 28 | break; 29 | } 30 | } 31 | 32 | return 0; 33 | } -------------------------------------------------------------------------------- /examples/screenshot.cpp: -------------------------------------------------------------------------------- 1 | #include "dxcam/dxcam.h" 2 | #include "opencv2/opencv.hpp" 3 | 4 | int main() { 5 | const auto devices_info = DXCam::get_devices_info(); 6 | const auto outputs_info = DXCam::get_outputs_info(); 7 | 8 | for (size_t i = 0; i < devices_info.size(); i++) { 9 | const auto &[description, vram_size, vendor_id] = devices_info[i]; 10 | printf("Device [%llu]: %ls VRam:%llu VendorId:%u\n", i, 11 | description.c_str(), vram_size, vendor_id); 12 | for (size_t j = 0; j < outputs_info[i].size(); j++) { 13 | const auto &[device_name, width, height, rotation_angle, 14 | is_primary] = outputs_info[i][j]; 15 | printf("\tOutput [%llu]: %ls Res:%ldx%ld Rot:%d IsPrimary:%d\n", j, 16 | device_name.c_str(), width, height, rotation_angle, 17 | is_primary); 18 | } 19 | } 20 | 21 | const auto camera = DXCam::create(); 22 | 23 | while (true) { 24 | auto frame = camera->grab(); 25 | if (frame.empty()) { 26 | std::cerr << "Failed to grab frame." << std::endl; 27 | continue; 28 | } 29 | 30 | cv::imshow("frame", frame); 31 | if (cv::waitKey(0) == 27) { // ESC 32 | break; 33 | } 34 | } 35 | 36 | return 0; 37 | } -------------------------------------------------------------------------------- /src/core/Device.cpp: -------------------------------------------------------------------------------- 1 | #include "Device.h" 2 | 3 | #include 4 | 5 | namespace DXCam { 6 | 7 | Device::Device(IDXGIAdapter1 *const adapter) : adapter(adapter), desc() { 8 | HRESULT hr = adapter->GetDesc1(&desc); 9 | assert(SUCCEEDED(hr)); 10 | 11 | constexpr D3D_FEATURE_LEVEL feature_levels[] = { 12 | D3D_FEATURE_LEVEL_11_0, 13 | D3D_FEATURE_LEVEL_10_1, 14 | D3D_FEATURE_LEVEL_10_0, 15 | }; 16 | constexpr size_t feature_levels_len = std::size(feature_levels); 17 | 18 | hr = D3D11CreateDevice(adapter, D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0, 19 | feature_levels, feature_levels_len, 7, &device, 20 | nullptr, &context); 21 | assert(SUCCEEDED(hr)); 22 | device->GetImmediateContext(&im_context); 23 | } 24 | 25 | std::vector Device::enum_outputs() const { 26 | std::vector p_outputs; 27 | IDXGIOutput1 *p_output; 28 | for (UINT i = 0; 29 | adapter->EnumOutputs(i, reinterpret_cast(&p_output)) != 30 | DXGI_ERROR_NOT_FOUND; 31 | i++) { 32 | p_outputs.emplace_back(p_output); 33 | } 34 | return p_outputs; 35 | } 36 | 37 | DeviceInfo Device::get_info() const { 38 | return {desc.Description, desc.DedicatedVideoMemory, desc.VendorId}; 39 | } 40 | 41 | } // namespace DXCam -------------------------------------------------------------------------------- /src/core/Output.cpp: -------------------------------------------------------------------------------- 1 | #include "Output.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace DXCam { 7 | 8 | Output::Output(IDXGIOutput1 *const output) : output(output), desc_() { 9 | update_desc(); 10 | } 11 | 12 | void Output::update_desc() { 13 | HRESULT hr = output->GetDesc(&desc_); 14 | assert(SUCCEEDED(hr)); 15 | } 16 | 17 | OutputInfo Output::get_info() const { 18 | LONG width, height; 19 | get_resolution(&width, &height); 20 | return {get_device_name(), width, height, get_rotation_angle()}; 21 | } 22 | 23 | std::wstring Output::get_device_name() const { return desc_.DeviceName; } 24 | 25 | void Output::get_resolution(LONG *width, LONG *height) const { 26 | *width = desc_.DesktopCoordinates.right - desc_.DesktopCoordinates.left; 27 | *height = desc_.DesktopCoordinates.bottom - desc_.DesktopCoordinates.top; 28 | } 29 | 30 | void Output::get_surface_size(LONG *width, LONG *height) const { 31 | LONG ret_width = 0; 32 | LONG ret_height = 0; 33 | get_resolution(&ret_width, &ret_height); 34 | if (get_rotation_angle() == 90 || get_rotation_angle() == 270) { 35 | std::swap(ret_width, ret_height); 36 | } 37 | *width = ret_width; 38 | *height = ret_height; 39 | } 40 | 41 | int Output::get_rotation_angle() const { 42 | return ROTATION_MAPPING[desc_.Rotation]; 43 | } 44 | 45 | } // namespace DXCam -------------------------------------------------------------------------------- /benchmarks/max_fps.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "dxcam/dxcam.h" 6 | 7 | // params 8 | const std::string TITLE = "[Benchmark] max_fps"; 9 | constexpr auto REGION = DXCam::Region{0, 0, 1920, 1080}; 10 | constexpr int TOTAL_FRAMES = 100; 11 | constexpr int REPEATS = 5; 12 | 13 | double bench(const std::shared_ptr& camera) { 14 | const auto begin_time = std::chrono::steady_clock::now(); 15 | for (int i = 0; i < TOTAL_FRAMES;) { 16 | auto frame = camera->grab(); 17 | if (!frame.empty()) { 18 | volatile auto data = std::move(frame); // avoid optimization 19 | i++; 20 | } 21 | } 22 | const auto end_time = std::chrono::steady_clock::now(); 23 | 24 | return std::chrono::duration(end_time - begin_time).count(); 25 | } 26 | 27 | int main() { 28 | // init 29 | const auto camera = DXCam::create(REGION); 30 | 31 | // benchmarks 32 | double duration = std::numeric_limits::max(); 33 | for (int i = 0; i < REPEATS; i++) { 34 | printf("Bench [%d / %d]\n", i + 1, REPEATS); 35 | duration = std::min(duration, bench(camera)); 36 | } 37 | 38 | // result 39 | const auto fps = TOTAL_FRAMES / duration; 40 | printf("%s: %d loops, best of %d: %lf FPS\n", TITLE.c_str(), TOTAL_FRAMES, 41 | REPEATS, fps); 42 | 43 | return 0; 44 | } -------------------------------------------------------------------------------- /python/src/py_DXCamera.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_PYTHON_PY_DXCAMERA_H 2 | #define DXCAM_CPP_PYTHON_PY_DXCAMERA_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "dxcam/DXCamera.h" 10 | #include "py_FrameBuffer.h" 11 | 12 | namespace py = pybind11; 13 | 14 | class DXCamera { 15 | public: 16 | DXCamera(std::shared_ptr &&camera, 17 | const std::string &output_color); 18 | 19 | void release(); 20 | 21 | [[nodiscard]] std::optional> grab( 22 | const std::optional ®ion) const; 23 | 24 | void start(const std::optional ®ion, int target_fps, 25 | bool video_mode, int delay) const; 26 | void stop() const; 27 | [[nodiscard]] py::array_t get_latest_frame() const; 28 | [[nodiscard]] FrameBuffer frame_buffer() const; 29 | 30 | [[nodiscard]] int get_width() const; 31 | [[nodiscard]] int get_height() const; 32 | [[nodiscard]] int get_channel_size() const; 33 | [[nodiscard]] int get_rotation_angle() const; 34 | [[nodiscard]] py::tuple get_region() const; 35 | [[nodiscard]] size_t get_max_buffer_len() const; 36 | [[nodiscard]] bool is_capturing() const; 37 | 38 | private: 39 | std::shared_ptr camera_; 40 | const cv::ColorConversionCodes cvt_color_flag_; 41 | const static std::unordered_map 42 | cvt_color_flag_map_; 43 | }; 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["scikit-build-core>=0.11", "pybind11>=3.0"] 3 | build-backend = "scikit_build_core.build" 4 | 5 | [project] 6 | name = "dxcam_cpp" 7 | version = "0.2.5" 8 | requires-python = ">=3.8" 9 | authors = [ 10 | { name = "Fidel Yin", email = "fidel.yin@hotmail.com" } 11 | ] 12 | description = "A high performance screen capturing library for Windows rewriting DXcam in C++" 13 | readme = "python/README.md" 14 | keywords = ["windows", "screenshot", "capture"] 15 | license = "MIT" 16 | classifiers = [ 17 | "Development Status :: 3 - Alpha", 18 | "Programming Language :: C++", 19 | "Operating System :: Microsoft :: Windows", 20 | "Operating System :: Microsoft :: Windows :: Windows 10", 21 | "Operating System :: Microsoft :: Windows :: Windows 11", 22 | "Intended Audience :: Developers", 23 | "Topic :: Multimedia :: Graphics :: Capture", 24 | "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", 25 | ] 26 | 27 | [project.urls] 28 | Homepage = "https://github.com/Fidelxyz/DXCam-CPP" 29 | Repository = "https://github.com/Fidelxyz/DXCam-CPP" 30 | Issues = "https://github.com/Fidelxyz/DXCam-CPP/issues" 31 | 32 | [tool.scikit-build] 33 | minimum-version = "build-system.requires" 34 | wheel.packages = ["python/src/dxcam_cpp"] 35 | 36 | [tool.scikit-build.cmake.define] 37 | CMAKE_TOOLCHAIN_FILE = { env = "CMAKE_TOOLCHAIN_FILE" } 38 | X_VCPKG_APPLOCAL_DEPS_INSTALL = true 39 | BUILD_PYTHON = true 40 | INSTALL_PYTHON_ONLY = true 41 | 42 | [tool.cibuildwheel] 43 | build-frontend = "build[uv]" 44 | archs = ['AMD64'] 45 | -------------------------------------------------------------------------------- /src/DXFactory.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_DXFACTORY_H 2 | #define DXCAM_CPP_DXFACTORY_H 3 | 4 | #include 5 | #include 6 | 7 | #include "DXCamera.h" 8 | #include "core/OutputMetadata.h" 9 | #include "../include/dxcam/core/DeviceInfo.h" 10 | #include "../include/dxcam/core/OutputInfo.h" 11 | #include "core/Device.h" 12 | #include "core/Output.h" 13 | 14 | namespace DXCam { 15 | 16 | class DXFactory { 17 | public: 18 | DXFactory() = default; 19 | 20 | std::shared_ptr create(int device_idx = 0, int output_idx = -1, 21 | size_t max_buffer_len = 64); 22 | std::shared_ptr create(const Region ®ion, int device_idx = 0, 23 | int output_idx = -1, 24 | size_t max_buffer_len = 64); 25 | 26 | [[nodiscard]] std::vector get_devices_info(); 27 | [[nodiscard]] std::vector> get_outputs_info(); 28 | 29 | std::vector devices; 30 | std::vector> outputs; 31 | OutputMetadata output_metadata; 32 | 33 | private: 34 | void init(); 35 | 36 | [[nodiscard]] int find_primary_output_idx(int device_idx) const; 37 | [[nodiscard]] std::shared_ptr find_instant(int device_idx, 38 | int output_idx); 39 | 40 | bool is_initialized_ = false; 41 | std::map, std::weak_ptr> camera_instants_; 42 | }; 43 | 44 | } // namespace DXCam 45 | 46 | #endif // DXCAM_CPP_DXFACTORY_H 47 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "configurePresets": [ 4 | { 5 | "name": "debug", 6 | "generator": "Ninja", 7 | "cacheVariables": { 8 | "CMAKE_BUILD_TYPE": "Debug", 9 | "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", 10 | "BUILD_PYTHON": { 11 | "type": "BOOL", 12 | "value": true 13 | }, 14 | "BUILD_EXAMPLES": { 15 | "type": "BOOL", 16 | "value": true 17 | }, 18 | "BUILD_BENCHMARKS": { 19 | "type": "BOOL", 20 | "value": true 21 | }, 22 | "X_VCPKG_APPLOCAL_DEPS_INSTALL": { 23 | "type": "BOOL", 24 | "value": true 25 | } 26 | }, 27 | "vendor": { 28 | "jetbrains.com/clion": { 29 | "toolchain": "Visual Studio" 30 | } 31 | } 32 | }, 33 | { 34 | "name": "release", 35 | "generator": "Ninja", 36 | "cacheVariables": { 37 | "CMAKE_BUILD_TYPE": "Release", 38 | "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", 39 | "BUILD_PYTHON": { 40 | "type": "BOOL", 41 | "value": true 42 | }, 43 | "BUILD_EXAMPLES": { 44 | "type": "BOOL", 45 | "value": true 46 | }, 47 | "BUILD_BENCHMARKS": { 48 | "type": "BOOL", 49 | "value": true 50 | }, 51 | "X_VCPKG_APPLOCAL_DEPS_INSTALL": { 52 | "type": "BOOL", 53 | "value": true 54 | } 55 | }, 56 | "vendor": { 57 | "jetbrains.com/clion": { 58 | "toolchain": "Visual Studio" 59 | } 60 | } 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /python/src/py_bindings.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "py_dxcam.h" 5 | 6 | namespace py = pybind11; 7 | 8 | PYBIND11_MODULE(dxcam_cpp, m) { 9 | m.doc() = "DXCam-CPP warped in Python"; 10 | 11 | m.def("device_info", &device_info, "List all outputs."); 12 | m.def("output_info", &output_info, "List all outputs."); 13 | 14 | m.def("create", &create, "Create a DXCamera instance.", 15 | py::arg("device_idx") = 0, py::arg("output_idx") = py::none(), 16 | py::arg("region") = py::none(), py::arg("output_color") = "RGB", 17 | py::arg("max_buffer_len") = 64); 18 | 19 | py::class_>(m, "DXCamera") 20 | .def("grab", &DXCamera::grab, py::arg("region") = py::none()) 21 | .def("start", &DXCamera::start, py::arg("region") = py::none(), 22 | py::arg("target_fps") = 60, py::arg("video_mode") = false, 23 | py::arg("delay") = 0) 24 | .def("stop", &DXCamera::stop) 25 | .def("get_latest_frame", &DXCamera::get_latest_frame) 26 | .def("release", &DXCamera::release) 27 | .def("frame_buffer", &DXCamera::frame_buffer) 28 | .def_property_readonly("width", &DXCamera::get_width) 29 | .def_property_readonly("height", &DXCamera::get_height) 30 | .def_property_readonly("channel_size", &DXCamera::get_channel_size) 31 | .def_property_readonly("rotation_angle", &DXCamera::get_rotation_angle) 32 | .def_property_readonly("region", &DXCamera::get_region) 33 | .def_property_readonly("max_buffer_len", &DXCamera::get_max_buffer_len) 34 | .def_property_readonly("is_capturing", &DXCamera::is_capturing); 35 | 36 | py::class_(m, "FrameBuffer") 37 | .def("__enter__", &FrameBuffer::enter) 38 | .def("__exit__", &FrameBuffer::exit); 39 | } 40 | -------------------------------------------------------------------------------- /src/util/io.cpp: -------------------------------------------------------------------------------- 1 | #include "util/io.h" 2 | 3 | #include 4 | 5 | namespace DXCam { 6 | 7 | std::vector enum_dxgi_adapters() { 8 | IDXGIFactory1 *dxgi_factory = nullptr; 9 | HRESULT hr = CreateDXGIFactory1(__uuidof(IDXGIFactory1), 10 | reinterpret_cast(&dxgi_factory)); 11 | assert(SUCCEEDED(hr)); 12 | 13 | std::vector adapters; 14 | IDXGIAdapter1 *adapter = nullptr; 15 | for (UINT i = 0; 16 | dxgi_factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND; 17 | i++) { 18 | adapters.emplace_back(adapter); 19 | adapter = nullptr; 20 | } 21 | return adapters; 22 | } 23 | 24 | OutputMetadata get_output_metadata() { 25 | OutputMetadata output_metadata; 26 | auto adapter = DISPLAY_DEVICEW(); 27 | adapter.cb = sizeof(adapter); 28 | for (DWORD i = 0; EnumDisplayDevicesW(nullptr, i, &adapter, 29 | EDD_GET_DEVICE_INTERFACE_NAME); 30 | i++) { 31 | if (adapter.StateFlags & DISPLAY_DEVICE_ACTIVE) { 32 | const bool is_primary = 33 | adapter.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE; 34 | AdapterMetadata adapter_metadata( 35 | adapter.DeviceName, adapter.DeviceString, is_primary, {}); 36 | 37 | auto display = DISPLAY_DEVICEW(); 38 | display.cb = sizeof(display); 39 | for (DWORD j = 0; 40 | EnumDisplayDevicesW(adapter.DeviceName, j, &display, 0); j++) { 41 | adapter_metadata.displays.emplace_back(display.DeviceName, 42 | display.DeviceString); 43 | } 44 | 45 | output_metadata.adapters.emplace_back(std::move(adapter_metadata)); 46 | } 47 | } 48 | return output_metadata; 49 | } 50 | 51 | } // namespace DXCam -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | import os 7 | import subprocess 8 | import sys 9 | from pathlib import Path 10 | 11 | DIR = Path(__file__).parent.resolve() 12 | 13 | # -- Project information ----------------------------------------------------- 14 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 15 | 16 | project = 'DXCam-CPP' 17 | copyright = '2025, Fidel Yin' 18 | author = 'Fidel Yin' 19 | 20 | # -- General configuration --------------------------------------------------- 21 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 22 | 23 | extensions = ['breathe'] 24 | 25 | templates_path = ['_templates'] 26 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 27 | 28 | breathe_projects = {"DXCam-CPP": "_build/xml/"} 29 | breathe_default_project = "DXCam-CPP" 30 | 31 | # -- Options for HTML output ------------------------------------------------- 32 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 33 | 34 | html_theme = 'furo' 35 | html_static_path = ['_static'] 36 | 37 | 38 | def generate_doxygen_xml(app): 39 | build_dir = os.path.join(app.confdir, "_build") 40 | if not os.path.exists(build_dir): 41 | os.mkdir(build_dir) 42 | 43 | try: 44 | subprocess.call(["doxygen", "--version"]) 45 | retcode = subprocess.call(["doxygen"], cwd=app.confdir) 46 | if retcode < 0: 47 | sys.stderr.write(f"doxygen error code: {-retcode}\n") 48 | except OSError as e: 49 | sys.stderr.write(f"doxygen execution failed: {e}\n") 50 | 51 | 52 | def setup(app): 53 | # Add hook for building doxygen xml when needed 54 | app.connect("builder-inited", generate_doxygen_xml) 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DXCam-CPP 2 | 3 | A high performance screen capturing library for Windows 4 | porting [DXcam](https://github.com/ra1nty/DXcam) to C++. 5 | 6 | [![PyPI - Version](https://img.shields.io/pypi/v/dxcam-cpp)](https://pypi.org/project/dxcam-cpp/) 7 | ![Vcpkg Version](https://img.shields.io/vcpkg/v/dxcam-cpp) 8 | [![CI](https://github.com/Fidelxyz/DXCam-CPP/actions/workflows/workflow.yml/badge.svg)](https://github.com/Fidelxyz/DXCam-CPP/actions/workflows/workflow.yml) 9 | [![GitHub License](https://img.shields.io/github/license/Fidelxyz/DXCam-CPP)](https://github.com/Fidelxyz/DXCam-CPP/blob/main/LICENSE) 10 | 11 | ## Usage 12 | 13 | See the [**documentation**](https://dxcam-cpp.readthedocs.io/) for detailed 14 | usage. 15 | 16 | ### C++ Library 17 | 18 | DXCam-CPP is available on [vcpkg](https://github.com/microsoft/vcpkg). 19 | 20 | Pre-built binaries are also available 21 | on the [Releases](https://github.com/Fidelxyz/DXCam-CPP/releases) page. 22 | 23 | ### Python Module 24 | 25 | The interface of the Python module of DXCam-CPP is designed to be **fully 26 | compatible** with the original [DXcam](https://github.com/ra1nty/DXcam) as an 27 | alternative. 28 | 29 | ## Build 30 | 31 | ### Dependencies 32 | 33 | - CMake 34 | - Visual Studio 2022 35 | - [OpenCV](https://github.com/opencv/opencv) 36 | 37 | #### For Python Bindings 38 | 39 | - [pybind11](https://github.com/pybind/pybind11) 40 | 41 | #### For Documentation 42 | 43 | - [Doxygen](https://github.com/doxygen/doxygen) 44 | - [Sphinx](https://github.com/sphinx-doc/sphinx) 45 | - [Breathe](https://github.com/breathe-doc/breathe) 46 | 47 | ### Build for C++ Library 48 | 49 | ```powershell 50 | cmake -B ./build --preset release 51 | cmake --build ./build 52 | cmake --install ./build --prefix ./build/install 53 | ``` 54 | 55 | ### Build for Python Package 56 | 57 | ```powershell 58 | $env:CMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" 59 | pip install . 60 | ``` 61 | -------------------------------------------------------------------------------- /src/core/StageSurface.cpp: -------------------------------------------------------------------------------- 1 | #include "StageSurface.h" 2 | 3 | #include 4 | 5 | namespace DXCam { 6 | 7 | StageSurface::StageSurface(const Output *const output, 8 | const Device *const device) { 9 | create(output, device); 10 | } 11 | 12 | StageSurface::~StageSurface() { release(); } 13 | 14 | void StageSurface::create(const Output *const output, 15 | const Device *const device) { 16 | output->get_surface_size(&width_, &height_); 17 | const D3D11_TEXTURE2D_DESC desc{static_cast(width_), 18 | static_cast(height_), 19 | 1, 20 | 1, 21 | dxgi_format_, 22 | {1, 0}, 23 | D3D11_USAGE_STAGING, 24 | 0, 25 | D3D11_CPU_ACCESS_READ, 26 | 0}; 27 | 28 | HRESULT hr = device->device->CreateTexture2D(&desc, nullptr, &texture); 29 | assert(SUCCEEDED(hr)); 30 | assert(texture != nullptr); 31 | 32 | hr = texture->QueryInterface(__uuidof(IDXGISurface), 33 | reinterpret_cast(&surface_)); 34 | assert(SUCCEEDED(hr)); 35 | } 36 | 37 | void StageSurface::release() { 38 | if (texture != nullptr) { 39 | texture->Release(); 40 | texture = nullptr; 41 | } 42 | } 43 | 44 | void StageSurface::rebuild(const Output *output, const Device *device) { 45 | release(); 46 | create(output, device); 47 | } 48 | 49 | DXGI_MAPPED_RECT StageSurface::map() const { 50 | DXGI_MAPPED_RECT rect; 51 | HRESULT hr = surface_->Map(&rect, DXGI_MAP_READ); 52 | assert(SUCCEEDED(hr)); 53 | 54 | return rect; 55 | } 56 | 57 | void StageSurface::unmap() const { 58 | HRESULT hr = surface_->Unmap(); 59 | assert(SUCCEEDED(hr)); 60 | } 61 | 62 | } // namespace DXCam -------------------------------------------------------------------------------- /python/src/py_dxcam.cpp: -------------------------------------------------------------------------------- 1 | #include "py_dxcam.h" 2 | 3 | #include 4 | 5 | #include "dxcam/dxcam.h" 6 | #include "util/string_cvt.h" 7 | 8 | std::string device_info() { 9 | const auto info = DXCam::get_devices_info(); 10 | std::string ret; 11 | for (size_t i = 0; i < info.size(); i++) { 12 | ret += std::format( 13 | "Device[{}]:\n", i, 14 | wstring_to_string(info[i].description), info[i].vram_size / 1048576, 15 | info[i].vendor_id); 16 | } 17 | return ret; 18 | } 19 | 20 | std::string output_info() { 21 | const auto info = DXCam::get_outputs_info(); 22 | std::string ret; 23 | for (size_t device_idx = 0; device_idx < info.size(); device_idx++) { 24 | for (size_t output_idx = 0; output_idx < info[device_idx].size(); 25 | output_idx++) { 26 | const auto &output = info[device_idx][output_idx]; 27 | ret += std::format( 28 | "Device[{}] Output[{}]: Res:({}, {}) Rot:{} Primary:{}\n", 29 | device_idx, output_idx, output.width, output.height, 30 | output.rotation_angle, output.is_primary ? "True" : "False"); 31 | } 32 | } 33 | return ret; 34 | } 35 | 36 | std::shared_ptr create(const int device_idx, 37 | const std::optional output_idx, 38 | const std::optional ®ion, 39 | const std::string &output_color, 40 | const size_t max_buffer_len) { 41 | const int output_idx_ = output_idx.value_or(-1); 42 | 43 | if (!region) { 44 | return std::make_shared( 45 | DXCam::create(device_idx, output_idx_, max_buffer_len), 46 | output_color); 47 | } 48 | 49 | const auto region_ = std::make_from_tuple( 50 | py::cast>(*region)); 51 | return std::make_shared( 52 | DXCam::create(region_, device_idx, output_idx_, max_buffer_len), 53 | output_color); 54 | } 55 | -------------------------------------------------------------------------------- /benchmarks/target_fps.cpp: -------------------------------------------------------------------------------- 1 | #include "dxcam/dxcam.h" 2 | 3 | // params 4 | const std::string TITLE = "target_fps"; 5 | constexpr auto REGION = DXCam::Region{0, 0, 1920, 1080}; 6 | constexpr int DURATION = 5; 7 | constexpr int TARGET_FPS[] = {30, 60, 90, 120, 165}; 8 | 9 | std::tuple bench(const std::shared_ptr &camera, 10 | const int target_fps) { 11 | const int total_frames = target_fps * DURATION; 12 | double square_sum = 0; 13 | 14 | camera->start(target_fps, true); 15 | 16 | const auto begin_time = std::chrono::steady_clock::now(); 17 | auto last_frame_time = begin_time; 18 | for (int i = 0; i < total_frames; i++) { 19 | // volatile to avoid optimization 20 | volatile auto frame = camera->get_latest_frame(); 21 | 22 | const auto current_time = std::chrono::steady_clock::now(); 23 | square_sum += std::pow( 24 | std::chrono::duration(current_time - last_frame_time) 25 | .count(), 26 | 2); 27 | last_frame_time = current_time; 28 | } 29 | const auto end_time = std::chrono::steady_clock::now(); 30 | 31 | camera->stop(); 32 | 33 | const auto duration = 34 | std::chrono::duration(end_time - begin_time).count(); 35 | const auto fps = total_frames / duration; 36 | 37 | const auto std = std::sqrt(square_sum / total_frames - 38 | std::pow(duration / total_frames, 2)); 39 | 40 | return {fps, std}; 41 | } 42 | 43 | int main() { 44 | std::printf("[DXCam-CPP Benchmark] %s\n\n", TITLE.c_str()); 45 | 46 | std::puts("Parameters:"); 47 | std::printf(" Region: (%d, %d, %d, %d)\n", REGION.left, REGION.top, 48 | REGION.right, REGION.bottom); 49 | std::printf(" Duration: %d s\n", DURATION); 50 | 51 | // init 52 | const auto camera = DXCam::create(REGION); 53 | 54 | // benchmark 55 | for (const auto &target_fps : TARGET_FPS) { 56 | std::printf("Bench: Target FPS = %d\n", target_fps); 57 | const auto [fps, std] = bench(camera, target_fps); 58 | std::printf("Result: Target FPS = %d: %lf FPS, std = %lf\n", target_fps, 59 | fps, std); 60 | } 61 | 62 | return 0; 63 | } -------------------------------------------------------------------------------- /src/core/Processor.cpp: -------------------------------------------------------------------------------- 1 | #include "core/Processor.h" 2 | 3 | namespace DXCam { 4 | 5 | cv::Mat Processor::process(const DXGI_MAPPED_RECT &rect, int width, int height, 6 | const Region ®ion, const int rotation_angle) { 7 | auto pitch = rect.Pitch; 8 | 9 | // Pre-crop 10 | int offset = 0; 11 | switch (rotation_angle) { 12 | case 0: 13 | offset = region.top * pitch; 14 | height = region.bottom - region.top; 15 | break; 16 | case 90: 17 | offset = (width - region.right) * pitch; 18 | width = region.right - region.left; 19 | break; 20 | case 180: 21 | offset = (height - region.bottom) * pitch; 22 | height = region.bottom - region.top; 23 | break; 24 | case 270: 25 | offset = region.left * pitch; 26 | width = region.right - region.left; 27 | break; 28 | default: 29 | assert(false); // never reach 30 | break; 31 | } 32 | 33 | pitch /= 4; // number of channels 34 | cv::Mat image; 35 | if (rotation_angle == 0 || rotation_angle == 180) { 36 | image = cv::Mat(height, pitch, CV_8UC4, rect.pBits + offset); 37 | if (pitch != width) { image = image.colRange(0, width); } 38 | } else { 39 | image = cv::Mat(width, pitch, CV_8UC4, rect.pBits + offset); 40 | if (pitch != height) { image = image.colRange(0, height); } 41 | } 42 | 43 | // Rotation 44 | switch (rotation_angle) { 45 | case 0: 46 | break; 47 | case 90: 48 | cv::rotate(image, image, cv::ROTATE_90_CLOCKWISE); 49 | break; 50 | case 180: 51 | cv::rotate(image, image, cv::ROTATE_180); 52 | break; 53 | case 270: 54 | cv::rotate(image, image, cv::ROTATE_90_COUNTERCLOCKWISE); 55 | break; 56 | default: 57 | assert(false); // never reach 58 | break; 59 | } 60 | 61 | // Crop 62 | if (region.right - region.left != width) { 63 | image = image.colRange(region.left, region.right); 64 | } else if (region.bottom - region.top != height) { 65 | image = image.rowRange(region.top, region.bottom); 66 | } 67 | 68 | return image; 69 | } 70 | 71 | } // namespace DXCam -------------------------------------------------------------------------------- /include/dxcam/dxcam.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_DXCAM_H 2 | #define DXCAM_CPP_DXCAM_H 3 | 4 | #define NOMINMAX // Disable min and max macros in 5 | 6 | #include "DXCamera.h" 7 | #include "core/DeviceInfo.h" 8 | #include "core/OutputInfo.h" 9 | 10 | namespace DXCam { 11 | 12 | /** 13 | * @brief Create a DXCamera instance which captures the full screen by default. 14 | * @param [in] device_idx The index of the device to be used. Call 15 | * get_devices_info() to list all devices. 16 | * @param [in] output_idx The index of the monitor to be used. Call 17 | * get_outputs_info() to list all outputs. 18 | * @param [in] max_buffer_len The size of the frame buffer. 19 | * @return A shared pointer to the DXCamera instance. If there exists an 20 | * instance with the same device and output, the shared pointer to the existing 21 | * instance will be returned. 22 | */ 23 | [[maybe_unused]] DXCAM_EXPORT std::shared_ptr create( 24 | int device_idx = 0, int output_idx = -1, size_t max_buffer_len = 64); 25 | 26 | /** 27 | * @brief Create a DXCamera instance which captures a rectangle region by 28 | * default. 29 | * @param [in] region The rectangle region to be captured. 30 | * @param [in] device_idx The index of the device to be used. Call 31 | * get_devices_info() to list all devices. 32 | * @param [in] output_idx The index of the monitor to be used. Call 33 | * get_outputs_info() to list all outputs. 34 | * @param [in] max_buffer_len The size of the frame buffer. 35 | * @return A shared pointer to the DXCamera instance. If there exists an 36 | * instance with the same device and output, the shared pointer to the existing 37 | * instance will be returned. 38 | */ 39 | [[maybe_unused]] DXCAM_EXPORT std::shared_ptr create( 40 | const Region ®ion, int device_idx = 0, int output_idx = -1, 41 | size_t max_buffer_len = 64); 42 | 43 | /** 44 | * @brief List all devices. 45 | * @return A vector of DeviceInfo. 46 | */ 47 | [[maybe_unused]] DXCAM_EXPORT std::vector get_devices_info(); 48 | /** 49 | * @brief List all outputs. 50 | * @return A vector of vectors containing OutputInfo. The first dimension 51 | * represents devices, and the second dimension represents outputs of that 52 | * device. 53 | */ 54 | [[maybe_unused]] DXCAM_EXPORT std::vector> 55 | get_outputs_info(); 56 | 57 | } // namespace DXCam 58 | 59 | #endif // DXCAM_CPP_DXCAM_H 60 | -------------------------------------------------------------------------------- /docs/cpp/quick_start.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | ########### 3 | 4 | If you are using CMake, you can use ``find_package`` to include DXCam-CPP in 5 | your project: 6 | 7 | .. code-block:: cmake 8 | 9 | find_package(DXCam CONFIG REQUIRED) 10 | target_link_libraries(... PRIVATE DXCam::DXCam) 11 | 12 | Include ``dxcam.h`` in your project: 13 | 14 | .. code-block:: cpp 15 | 16 | #include 17 | 18 | Everything provided by DXCam-CPP is in the ``DXCam::`` namespace. 19 | 20 | Most of the usage is the same as the original Python DXCam, **but the full 21 | consistency the interface of C++ library with the original Python version is not 22 | guaranteed**. 23 | 24 | Initialization 25 | ============== 26 | 27 | To create a DXCamera instance, you may call :cpp:func:`DXCam::create`, which 28 | returns a shared pointer to the :cpp:class:`DXCam::DXCamera` instance. 29 | 30 | .. code-block:: cpp 31 | 32 | std::shared_ptr camera = DXCam::create(); 33 | 34 | Screenshot a single frame 35 | ========================= 36 | 37 | .. code-block:: cpp 38 | 39 | cv::Mat frame = camera.grab(); 40 | 41 | It is worth noting that :cpp:func:`DXCam::DXCamera::grab` will return an empty 42 | ``cv::Mat`` such that ``cv::Mat.empty() == true`` if there is no new frame since 43 | the last time you called :cpp:func:`DXCam::DXCamera::grab`. 44 | 45 | To screenshot a specific region, set the ``region`` argument which takes 46 | :cpp:class:`DXCam::Region{int left, int top, int right, int bottom} ` 47 | as the left, top, right, bottom coordinates of the bounding box. 48 | 49 | .. code-block:: cpp 50 | 51 | cv::Mat frame = camera.grab({0, 0, 1920, 1080}); 52 | 53 | Capturing screen continuously 54 | ============================= 55 | 56 | To start a screen capture, simply call :cpp:func:`DXCam::DXCamera::start`: the 57 | capture will be started in a separated thread, default at 60Hz. Call 58 | :cpp:func:`DXCam::DXCamera::stop` to stop the capture. 59 | 60 | .. code-block:: cpp 61 | 62 | camera.start(); 63 | assert(camera.is_capturing() == true); 64 | // do something 65 | camera.stop(); 66 | assert(camera.is_capturing() == false); 67 | 68 | While the :cpp:class:`DXCam::DXCamera` instance is in capturing mode, you may 69 | call :cpp:func:`DXCam::DXCamera::get_latest_frame` to get the latest frame in 70 | the frame buffer: 71 | 72 | .. code-block:: cpp 73 | 74 | cv::Mat frame = camera.get_latest_frame(); 75 | 76 | Notice that :cpp:func:`DXCam::DXCamera::get_latest_frame` by default will block 77 | until there is a new frame available since the previous call. To change this 78 | behavior, set the ``video_mode`` argument to ``true``. See the :ref:`video_mode` 79 | section for more details. 80 | -------------------------------------------------------------------------------- /src/core/Duplicator.cpp: -------------------------------------------------------------------------------- 1 | #include "Duplicator.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace DXCam { 7 | 8 | Duplicator::Duplicator(const Output *const output, const Device *const device) { 9 | create(output, device); 10 | } 11 | 12 | Duplicator::~Duplicator() { release(); } 13 | 14 | void Duplicator::create(const Output *const output, 15 | const Device *const device) { 16 | HRESULT hr = output->output->DuplicateOutput(device->device, &duplicator_); 17 | if (hr == DXGI_ERROR_UNSUPPORTED) { 18 | std::cerr << "IDXGIOutput1::DuplicateOutput failed: " 19 | "DXGI_ERROR_UNSUPPORTED.\n" 20 | "If you are running this application on a Microsoft " 21 | "Hybrid system, try to run the application on the " 22 | "integrated GPU instead of on the discrete GPU." 23 | << std::endl; 24 | throw std::runtime_error( 25 | "IDXGIOutput1::DuplicateOutput failed: " 26 | "DXGI_ERROR_UNSUPPORTED."); 27 | } 28 | if (hr == E_ACCESSDENIED) { 29 | std::cerr << "IDXGIOutput1::DuplicateOutput failed: E_ACCESSDENIED." 30 | << std::endl; 31 | return; 32 | } 33 | assert(SUCCEEDED(hr)); 34 | assert(duplicator_ != nullptr); 35 | } 36 | 37 | void Duplicator::release() { 38 | if (duplicator_ != nullptr) { 39 | duplicator_->Release(); 40 | duplicator_ = nullptr; 41 | } 42 | } 43 | 44 | void Duplicator::rebuild(const Output *const output, 45 | const Device *const device) { 46 | release(); 47 | create(output, device); 48 | } 49 | 50 | bool Duplicator::update_frame() { 51 | if (duplicator_ == nullptr) return false; 52 | 53 | DXGI_OUTDUPL_FRAME_INFO info; 54 | IDXGIResource *res = nullptr; 55 | HRESULT hr = duplicator_->AcquireNextFrame(0, &info, &res); 56 | if (hr == DXGI_ERROR_ACCESS_LOST) { return false; } 57 | if (hr == DXGI_ERROR_WAIT_TIMEOUT) { 58 | updated = false; 59 | return true; 60 | } 61 | if (hr == DXGI_ERROR_INVALID_CALL) { 62 | std::cerr << "IDXGIOutputDuplication::AcquireNextFrame failed: " 63 | "DXGI_ERROR_INVALID_CALL. Previous frame has not been " 64 | "released.\n" 65 | << "You probably called grab() when continuously capturing." 66 | << std::endl; 67 | return false; 68 | } 69 | assert(SUCCEEDED(hr)); 70 | 71 | hr = res->QueryInterface(__uuidof(ID3D11Texture2D), 72 | reinterpret_cast(&texture)); 73 | if (FAILED(hr)) { 74 | hr = duplicator_->ReleaseFrame(); 75 | assert(SUCCEEDED(hr)); 76 | } 77 | 78 | updated = true; 79 | return true; 80 | } 81 | 82 | void Duplicator::release_frame() const { 83 | HRESULT hr = duplicator_->ReleaseFrame(); 84 | assert(SUCCEEDED(hr) || hr == DXGI_ERROR_INVALID_CALL); 85 | } 86 | 87 | } // namespace DXCam -------------------------------------------------------------------------------- /python/src/py_DXCamera.cpp: -------------------------------------------------------------------------------- 1 | #include "py_DXCamera.h" 2 | 3 | #include 4 | 5 | #include "util/numpy.h" 6 | 7 | const std::unordered_map 8 | DXCamera::cvt_color_flag_map_ = { 9 | {"RGB", cv::COLOR_BGRA2RGB}, {"RGBA", cv::COLOR_BGRA2RGBA}, 10 | {"BGR", cv::COLOR_BGRA2BGR}, {"GRAY", cv::COLOR_BGRA2GRAY}, 11 | {"BGRA", cv::COLOR_COLORCVT_MAX}, // no conversion 12 | }; 13 | 14 | DXCamera::DXCamera(std::shared_ptr &&camera, 15 | const std::string &output_color) 16 | : camera_(std::move(camera)), 17 | cvt_color_flag_(cvt_color_flag_map_.at(output_color)) {} 18 | 19 | void DXCamera::release() { 20 | assert(camera_.use_count() == 1); 21 | camera_.reset(); 22 | } 23 | 24 | std::optional> DXCamera::grab( 25 | const std::optional ®ion) const { 26 | cv::Mat frame; 27 | if (!region) { 28 | frame = camera_->grab(); 29 | } else { 30 | const auto region_ = std::make_from_tuple( 31 | py::cast>(*region)); 32 | frame = camera_->grab(region_); 33 | } 34 | 35 | if (frame.empty()) { return std::nullopt; } 36 | 37 | if (cvt_color_flag_ != cv::COLOR_COLORCVT_MAX) { 38 | cv::cvtColor(frame, frame, cvt_color_flag_); 39 | } 40 | 41 | return numpy_array_from(std::move(frame)); 42 | } 43 | 44 | void DXCamera::start(const std::optional ®ion, 45 | const int target_fps, const bool video_mode, 46 | const int delay) const { 47 | if (!region) { 48 | camera_->start(target_fps, video_mode, delay); 49 | } else { 50 | const auto region_ = std::make_from_tuple( 51 | py::cast>(*region)); 52 | camera_->start(region_, target_fps, video_mode, delay); 53 | } 54 | } 55 | 56 | void DXCamera::stop() const { camera_->stop(); } 57 | 58 | py::array_t DXCamera::get_latest_frame() const { 59 | cv::Mat frame = camera_->get_latest_frame(); 60 | 61 | if (cvt_color_flag_ != cv::COLOR_COLORCVT_MAX) { 62 | cv::cvtColor(frame, frame, cvt_color_flag_); 63 | } 64 | 65 | return numpy_array_from(std::move(frame)); 66 | } 67 | 68 | FrameBuffer DXCamera::frame_buffer() const { return FrameBuffer(camera_); }; 69 | 70 | int DXCamera::get_width() const { return camera_->get_width(); } 71 | 72 | int DXCamera::get_height() const { return camera_->get_height(); } 73 | 74 | int DXCamera::get_channel_size() const { 75 | switch (cvt_color_flag_) { 76 | case cv::COLOR_BGRA2RGB: 77 | case cv::COLOR_BGRA2BGR: 78 | return 3; 79 | case cv::COLOR_BGRA2RGBA: 80 | case cv::COLOR_COLORCVT_MAX: // BGRA 81 | return 4; 82 | case cv::COLOR_BGRA2GRAY: 83 | return 1; 84 | default: 85 | assert(false); 86 | return 0; 87 | } 88 | } 89 | 90 | int DXCamera::get_rotation_angle() const { 91 | return camera_->get_rotation_angle(); 92 | } 93 | 94 | py::tuple DXCamera::get_region() const { 95 | const auto &[left, top, right, bottom] = camera_->get_region(); 96 | return py::make_tuple(left, right, top, bottom); 97 | } 98 | 99 | size_t DXCamera::get_max_buffer_len() const { 100 | return camera_->get_buffer_len(); 101 | } 102 | 103 | bool DXCamera::is_capturing() const { return camera_->is_capturing(); } 104 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | project(DXCam_CPP 3 | VERSION 0.2.5 4 | LANGUAGES CXX 5 | ) 6 | 7 | # Set default install directory 8 | if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 9 | set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/install" CACHE PATH "Installation Directory" FORCE) 10 | endif () 11 | 12 | # Require out-of-source builds 13 | file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/CMakeLists.txt" LOC_PATH) 14 | if (EXISTS "${LOC_PATH}") 15 | message(FATAL_ERROR "You cannot build in a source directory (or any directory with a CMakeLists.txt file). Please make a build subdirectory. Feel free to remove CMakeCache.txt and CMakeFiles.") 16 | endif () 17 | 18 | # Configuration options 19 | set(BUILD_BENCHMARKS OFF CACHE BOOL "Build benchmarks") 20 | set(BUILD_EXAMPLES OFF CACHE BOOL "Build examples") 21 | set(BUILD_PYTHON OFF CACHE BOOL "Build Python bindings") 22 | set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared libraries") 23 | set(INSTALL_PYTHON_ONLY OFF CACHE BOOL "Install Python module ONLY") 24 | 25 | set(CMAKE_INSTALL_BINDIR bin) 26 | set(CMAKE_INSTALL_LIBDIR lib) 27 | set(CMAKE_INSTALL_INCLUDEDIR include) 28 | set(INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR}) 29 | 30 | # Check dependencies 31 | # Require MSVC 32 | if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 33 | message(FATAL_ERROR "DXCam_CPP requires MSVC") 34 | endif () 35 | 36 | message(STATUS "Configuring DXCam_CPP") 37 | 38 | # Set compiler options 39 | if (CMAKE_BUILD_TYPE STREQUAL "Debug") 40 | add_compile_options(/Od /Zi) 41 | else () 42 | add_compile_options(/O2 /GL) 43 | add_link_options(/LTCG) 44 | endif () 45 | add_compile_options(/W4) 46 | 47 | find_package(OpenCV REQUIRED core imgcodecs) 48 | 49 | # DXCam 50 | add_library(DXCam 51 | src/core/Device.cpp 52 | src/core/Duplicator.cpp 53 | src/core/Output.cpp 54 | src/core/OutputMetadata.cpp 55 | src/core/Processor.cpp 56 | src/core/Region.cpp 57 | src/core/StageSurface.cpp 58 | src/util/HighResTimer.cpp 59 | src/util/io.cpp 60 | src/dxcam.cpp 61 | src/DXCamera.cpp 62 | src/DXFactory.cpp 63 | ) 64 | target_include_directories(DXCam 65 | PRIVATE 66 | src 67 | include/dxcam 68 | ${OpenCV_INCLUDE_DIRS} 69 | PUBLIC 70 | $ 71 | $ 72 | $ 73 | ) 74 | target_link_libraries(DXCam 75 | PUBLIC d3d11 dxgi ${OpenCV_LIBS} 76 | ) 77 | target_compile_features(DXCam 78 | PUBLIC cxx_std_20 79 | ) 80 | set_target_properties(DXCam 81 | PROPERTIES CXX_EXTENSIONS OFF 82 | DEBUG_POSTFIX d 83 | ) 84 | include(GenerateExportHeader) 85 | generate_export_header(DXCam 86 | EXPORT_FILE_NAME ${PROJECT_BINARY_DIR}/exports/dxcam_export.h 87 | ) 88 | 89 | # Install 90 | if (NOT INSTALL_PYTHON_ONLY) 91 | install(TARGETS DXCam 92 | EXPORT DXCamTargets 93 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 94 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 95 | ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} 96 | INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} 97 | ) 98 | install(DIRECTORY include DESTINATION .) 99 | install(FILES 100 | ${PROJECT_BINARY_DIR}/exports/dxcam_export.h 101 | DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) 102 | 103 | include(CMakePackageConfigHelpers) 104 | write_basic_package_version_file( 105 | DXCamConfigVersion.cmake 106 | VERSION ${PACKAGE_VERSION} 107 | COMPATIBILITY AnyNewerVersion 108 | ) 109 | install(EXPORT DXCamTargets 110 | FILE DXCamTargets.cmake 111 | NAMESPACE DXCam:: 112 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/dxcam 113 | ) 114 | configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/DXCamConfig.cmake.in 115 | "${CMAKE_CURRENT_BINARY_DIR}/DXCamConfig.cmake" 116 | INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/dxcam 117 | PATH_VARS INCLUDE_INSTALL_DIR 118 | ) 119 | install(FILES 120 | "${CMAKE_CURRENT_BINARY_DIR}/DXCamConfig.cmake" 121 | "${CMAKE_CURRENT_BINARY_DIR}/DXCamConfigVersion.cmake" 122 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/dxcam 123 | ) 124 | 125 | # Export 126 | export(TARGETS DXCam 127 | NAMESPACE DXCam:: 128 | FILE DXCamTargets.cmake 129 | ) 130 | export(PACKAGE DXCam) 131 | endif () 132 | 133 | message(STATUS "Configuring DXCam_CPP - done") 134 | 135 | if (BUILD_BENCHMARKS) 136 | add_subdirectory(benchmarks) 137 | endif () 138 | 139 | if (BUILD_EXAMPLES) 140 | add_subdirectory(examples) 141 | endif () 142 | 143 | if (BUILD_PYTHON) 144 | add_subdirectory(python) 145 | endif () 146 | -------------------------------------------------------------------------------- /src/DXFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "DXFactory.h" 2 | 3 | #include 4 | 5 | #include "util/io.h" 6 | 7 | namespace DXCam { 8 | 9 | void DXFactory::init() { 10 | if (is_initialized_) return; 11 | 12 | const DPI_AWARENESS_CONTEXT old_dpi_awareness = 13 | SetThreadDpiAwarenessContext( 14 | DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); 15 | 16 | const auto p_adapters = enum_dxgi_adapters(); 17 | for (const auto &p_adapter : p_adapters) { 18 | auto device = Device(p_adapter); 19 | const auto p_outputs = device.enum_outputs(); 20 | if (!p_outputs.empty()) { 21 | devices.emplace_back(device); 22 | outputs.emplace_back(p_outputs.begin(), p_outputs.end()); 23 | // in-place construct std::vector(p_outputs.begin(), 24 | // p_outputs.end()) 25 | } 26 | } 27 | output_metadata = get_output_metadata(); 28 | 29 | SetThreadDpiAwarenessContext(old_dpi_awareness); 30 | 31 | is_initialized_ = true; 32 | } 33 | 34 | std::shared_ptr DXFactory::create(const int device_idx, 35 | int output_idx, 36 | const size_t max_buffer_len) { 37 | init(); 38 | 39 | if (output_idx == -1) { output_idx = find_primary_output_idx(device_idx); } 40 | 41 | if (auto instant_ptr = find_instant(device_idx, output_idx)) { 42 | printf( 43 | "You already created a DXCamera Instance for Device %d--Output " 44 | "%d!\n", 45 | device_idx, output_idx); 46 | return instant_ptr; 47 | } 48 | 49 | auto const device = &devices[device_idx]; 50 | auto const output = &outputs[device_idx][output_idx]; 51 | 52 | output->update_desc(); 53 | 54 | long width, height; 55 | output->get_resolution(&width, &height); 56 | const Region region = {0, 0, width, height}; 57 | 58 | auto camera = std::make_shared(output, device, region, false, 59 | max_buffer_len); 60 | camera_instants_[std::make_tuple(device_idx, output_idx)] = camera; 61 | std::this_thread::sleep_for(std::chrono::nanoseconds(100000)); 62 | return camera; 63 | } 64 | 65 | std::shared_ptr DXFactory::create(const Region ®ion, 66 | const int device_idx, 67 | int output_idx, 68 | const size_t max_buffer_len) { 69 | init(); 70 | 71 | if (output_idx == -1) { output_idx = find_primary_output_idx(device_idx); } 72 | 73 | if (auto instant_ptr = find_instant(device_idx, output_idx)) { 74 | printf( 75 | "You already created a DXCamera Instance for Device %d--Output " 76 | "%d!\n", 77 | device_idx, output_idx); 78 | return instant_ptr; 79 | } 80 | 81 | auto const device = &devices[device_idx]; 82 | auto const output = &outputs[device_idx][output_idx]; 83 | 84 | output->update_desc(); 85 | 86 | auto camera = std::make_shared(output, device, region, true, 87 | max_buffer_len); 88 | camera_instants_[std::make_tuple(device_idx, output_idx)] = camera; 89 | std::this_thread::sleep_for(std::chrono::nanoseconds(100000)); 90 | return camera; 91 | } 92 | 93 | std::vector DXFactory::get_devices_info() { 94 | init(); 95 | 96 | std::vector devices_info; 97 | for (const auto &device : devices) { 98 | devices_info.emplace_back(device.get_info()); 99 | } 100 | return devices_info; 101 | } 102 | 103 | std::vector> DXFactory::get_outputs_info() { 104 | init(); 105 | 106 | std::vector> outputs_info; 107 | for (const auto &device_outputs : outputs) { 108 | std::vector device_outputs_info; 109 | for (const auto &output : device_outputs) { 110 | OutputInfo output_info = output.get_info(); 111 | output_info.is_primary = 112 | output_metadata.get(output.get_device_name()).is_primary; 113 | device_outputs_info.emplace_back(std::move(output_info)); 114 | } 115 | outputs_info.emplace_back(std::move(device_outputs_info)); 116 | } 117 | return outputs_info; 118 | } 119 | 120 | int DXFactory::find_primary_output_idx(const int device_idx) const { 121 | for (int output_idx = 0; output_idx < output_metadata.adapters.size(); 122 | output_idx++) { 123 | if (output_metadata 124 | .get(outputs[device_idx][output_idx].get_device_name()) 125 | .is_primary) { 126 | return output_idx; 127 | } 128 | } 129 | return -1; 130 | } 131 | 132 | std::shared_ptr DXFactory::find_instant(const int device_idx, 133 | const int output_idx) { 134 | const auto instant_key = std::make_tuple(device_idx, output_idx); 135 | decltype(camera_instants_)::iterator instant; 136 | while ((instant = camera_instants_.find(instant_key)) != 137 | camera_instants_.end()) { 138 | // found existing instance not expired 139 | if (!instant->second.expired()) return instant->second.lock(); 140 | 141 | // found existing instance expired 142 | camera_instants_.erase(instant); 143 | } 144 | return nullptr; 145 | } 146 | 147 | } // namespace DXCam -------------------------------------------------------------------------------- /docs/cpp/advanced.rst: -------------------------------------------------------------------------------- 1 | Advanced Usage and Remarks 2 | ########################## 3 | 4 | Multiple monitors / GPUs 5 | ======================== 6 | 7 | .. code-block:: cpp 8 | 9 | std::shared_ptr cam1 = DXCam::create(0, 0); 10 | std::shared_ptr cam2 = DXCam::create(0, 1); 11 | std::shared_ptr cam3 = DXCam::create(1, 1); 12 | cv::Mat img1 = cam1->grab(); 13 | cv::Mat img2 = cam2->grab(); 14 | cv::Mat img3 = cam3->grab(); 15 | 16 | The above code creates three :cpp:class:`DXCam::DXCamera` instances for: 17 | ``[monitor0, GPU0], [monitor1, GPU0], [monitor1, GPU1]``, and subsequently takes 18 | three full-screen screenshots. (Like Python DXcam, cross-GPU untested, but I 19 | also hope it works.) 20 | 21 | To get a complete list of devices and outputs, call 22 | :cpp:func:`DXCam::get_devices_info` and :cpp:func:`DXCam::get_outputs_info`: 23 | 24 | .. code-block:: cpp 25 | 26 | std::vector devices_info = DXCam::get_devices_info(); 27 | std::vector> outputs_info = DXCam::get_outputs_info(); 28 | 29 | Here is the declaration of :cpp:struct:`DXCam::DeviceInfo` and 30 | :cpp:struct:`DXCam::OutputInfo`: 31 | 32 | .. code-block:: cpp 33 | 34 | struct DXCam::DeviceInfo { 35 | std::wstring description; 36 | SIZE_T vram_size; 37 | UINT vendor_id; 38 | }; 39 | 40 | struct DXCam::OutputInfo { 41 | std::wstring device_name; 42 | LONG width; 43 | LONG height; 44 | int rotation_angle; 45 | }; 46 | 47 | Video buffer 48 | ============ 49 | 50 | The captured frames will be insert into a fixed-size ring buffer, and when the 51 | buffer is full the newest frame will replace the oldest frame. You can specify 52 | the maximum buffer length (default to 64) by setting the argument 53 | ``max_buffer_len`` upon creation of the :cpp:class:`DXCam::DXCamera` instance. 54 | 55 | .. code-block:: cpp 56 | 57 | DXCam::DXCamera camera = DXCam::create(0, -1, 512); 58 | 59 | Consuming frames from buffer 60 | ============================ 61 | 62 | To consume frames during capturing there is 63 | :cpp:func:`DXCam::DXCamera::get_latest_frame` available which assume the user to 64 | process frames in a LIFO pattern. This is a read-only action and won't pop the 65 | processed frame from the buffer. 66 | 67 | Going further than the Python DXcam, you may call 68 | :cpp:func:`DXCam::DXCamera::get_frame_buffer` to obtain the whole frame buffer: 69 | 70 | .. code-block:: cpp 71 | 72 | const cv::Mat *const *frame_buffer; 73 | const std::atomic_int *head, *tail; 74 | const size_t *len; 75 | const std::atomic_bool *full; 76 | std::mutex *const *frame_buffer_mutex; 77 | std::mutex *frame_buffer_all_mutex; 78 | 79 | camera.get_frame_buffer(&frame_buffer, &frame_buffer_mutex, &len, &head, 80 | &tail, &full, &frame_buffer_all_mutex); 81 | 82 | { 83 | // you should lock frame_buffer_all_mutex when reading the frame buffer 84 | std::scoped_lock lock(*frame_buffer_all_mutex); 85 | 86 | // read the frame buffer in a correct order 87 | for (size_t i = *head; i != *tail; i = (i + 1) % *len) { 88 | cv::Mat frame = (*frame_buffer)[i]; 89 | 90 | // Do something with the frame 91 | } 92 | } 93 | 94 | The frame buffer is readonly. 95 | 96 | .. important:: 97 | 98 | You should lock ``frame_buffer_all_mutex`` while reading the frame buffer. 99 | 100 | Target FPS 101 | ========== 102 | 103 | To set the target FPS, set the ``target_fps`` argument upon calling 104 | :cpp:func:`DXCam::DXCamera::start`. 105 | 106 | .. code-block:: cpp 107 | 108 | camera.start(60); 109 | 110 | .. _video_mode: 111 | 112 | Video mode 113 | ========== 114 | 115 | The default behavior of 116 | :cpp:func:`DXCam::DXCamera::get_latest_frame` only put newly rendered frames in 117 | the buffer, which suits the usage scenario of an object detection or machine 118 | learning pipeline. However, when recording a video that is not ideal since we 119 | aim to get the frames at a constant framerate. 120 | 121 | To capture frames at a constant framerate, you may set the ``video_mode`` 122 | argument to ``true`` when calling :cpp:func:`DXCam::DXCamera::start`, so that 123 | the frame buffer will be fed at the target fps. If there is no new frame 124 | available, the last rendered frame will be duplicated and put into the buffer. 125 | 126 | Safely releasing of resource 127 | ============================ 128 | 129 | Upon calling :cpp:func:`DXCam::DXCamera::stop`, DXCamera will stop the active 130 | capture and free the frame buffer. 131 | 132 | Upon deleting all ``std::shared_ptr`` objects referring to a 133 | :cpp:class:`DXCam::DXCamera` object, it will stop any active capturing, free the 134 | buffer, and release the duplicator and staging resource. 135 | 136 | Actually, you do not need to manually handle the resource releasing since 137 | ``std::shared_ptr`` will automatically manage the lifetime of the object it 138 | refers to. However, if you want to manually release the resource or recreate a 139 | :cpp:class:`DXCam::DXCamera` instance, you may manually delete it by calling 140 | ``std::shared_ptr::reset()`` on the ``std::shared_ptr`` object. 141 | 142 | .. code-block:: cpp 143 | 144 | std::shared_ptr camera1 = DXCam::create(0); 145 | std::shared_ptr camera2 = DXCam::create(0); // Not allowed, camera1 will be returned 146 | assert(camera1 == camera2); 147 | camera1.reset(); 148 | camera2 = DXCam::create(0); // Allowed -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "*" ] 6 | paths-ignore: 7 | - "README.md" 8 | - "LICENSE" 9 | - "docs/**" 10 | - ".readthedocs.yaml" 11 | pull_request: 12 | branches: [ "*" ] 13 | paths-ignore: 14 | - "README.md" 15 | - "LICENSE" 16 | - "docs/**" 17 | - ".readthedocs.yaml" 18 | release: 19 | types: [ published ] 20 | workflow_dispatch: 21 | inputs: 22 | publish: 23 | description: "Publish to PyPI" 24 | required: false 25 | type: boolean 26 | default: false 27 | 28 | permissions: 29 | packages: write 30 | 31 | env: 32 | PROJECT_NAME: "dxcam_cpp" 33 | USERNAME: ${{ github.repository_owner }} 34 | FEED_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json 35 | VCPKG_BINARY_SOURCES: "clear;nuget,https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json,readwrite" 36 | 37 | jobs: 38 | prebuild-vcpkg: 39 | runs-on: windows-latest 40 | 41 | steps: 42 | - uses: actions/checkout@v5 43 | with: 44 | submodules: true 45 | 46 | - name: Setup Developer Command Prompt 47 | uses: ilammy/msvc-dev-cmd@v1 48 | 49 | - name: Add NuGet sources 50 | shell: pwsh 51 | run: | 52 | .$(."${{ env.VCPKG_ROOT }}/vcpkg" fetch nuget) ` 53 | sources add ` 54 | -Source "${{ env.FEED_URL }}" ` 55 | -StorePasswordInClearText ` 56 | -Name GitHubPackages ` 57 | -UserName "${{ env.USERNAME }}" ` 58 | -Password "${{ secrets.GITHUB_TOKEN }}" 59 | .$(."${{ env.VCPKG_ROOT }}/vcpkg" fetch nuget) ` 60 | setapikey "${{ secrets.GITHUB_TOKEN }}" ` 61 | -Source "${{ env.FEED_URL }}" 62 | 63 | - name: Configure vcpkg 64 | run: ."${{ env.VCPKG_ROOT }}/vcpkg" integrate install 65 | 66 | - name: Install vcpkg dependencies 67 | run: ."${{ env.VCPKG_ROOT }}/vcpkg" install 68 | 69 | build: 70 | runs-on: windows-latest 71 | needs: [ prebuild-vcpkg ] 72 | 73 | steps: 74 | - uses: actions/checkout@v5 75 | with: 76 | submodules: true 77 | 78 | - name: Setup Developer Command Prompt 79 | uses: ilammy/msvc-dev-cmd@v1 80 | 81 | - name: Add NuGet sources 82 | shell: pwsh 83 | run: | 84 | .$(."${{ env.VCPKG_ROOT }}/vcpkg" fetch nuget) ` 85 | sources add ` 86 | -Source "${{ env.FEED_URL }}" ` 87 | -StorePasswordInClearText ` 88 | -Name GitHubPackages ` 89 | -UserName "${{ env.USERNAME }}" ` 90 | -Password "${{ secrets.GITHUB_TOKEN }}" 91 | .$(."${{ env.VCPKG_ROOT }}/vcpkg" fetch nuget) ` 92 | setapikey "${{ secrets.GITHUB_TOKEN }}" ` 93 | -Source "${{ env.FEED_URL }}" 94 | 95 | - name: Configure CMake 96 | run: >- 97 | cmake 98 | -B ${{ github.workspace }}/build 99 | -G Ninja 100 | -DCMAKE_C_COMPILER="cl.exe" 101 | -DCMAKE_CXX_COMPILER="cl.exe" 102 | -DCMAKE_BUILD_TYPE="Release" 103 | -DCMAKE_TOOLCHAIN_FILE="${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake" 104 | -DX_VCPKG_APPLOCAL_DEPS_INSTALL:BOOL="ON" 105 | 106 | - name: Build 107 | run: cmake --build "${{ github.workspace }}/build" 108 | 109 | - name: Install 110 | run: cmake --install "${{ github.workspace }}/build" 111 | 112 | - uses: actions/upload-artifact@v5 113 | with: 114 | name: ${{ env.PROJECT_NAME }}-win_amd64 115 | path: ${{ github.workspace }}/build/install 116 | 117 | build-wheels: 118 | runs-on: windows-latest 119 | needs: [ prebuild-vcpkg ] 120 | 121 | steps: 122 | - uses: actions/checkout@v5 123 | with: 124 | submodules: true 125 | 126 | - name: Setup Developer Command Prompt 127 | uses: ilammy/msvc-dev-cmd@v1 128 | 129 | - name: Add NuGet sources 130 | shell: pwsh 131 | run: | 132 | .$(."${{ env.VCPKG_ROOT }}/vcpkg" fetch nuget) ` 133 | sources add ` 134 | -Source "${{ env.FEED_URL }}" ` 135 | -StorePasswordInClearText ` 136 | -Name GitHubPackages ` 137 | -UserName "${{ env.USERNAME }}" ` 138 | -Password "${{ secrets.GITHUB_TOKEN }}" 139 | .$(."${{ env.VCPKG_ROOT }}/vcpkg" fetch nuget) ` 140 | setapikey "${{ secrets.GITHUB_TOKEN }}" ` 141 | -Source "${{ env.FEED_URL }}" 142 | 143 | - uses: astral-sh/setup-uv@v7 144 | 145 | - name: Build Python wheel 146 | uses: pypa/cibuildwheel@v3.3 147 | env: 148 | CMAKE_TOOLCHAIN_FILE: ${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake 149 | CMAKE_GENERATOR: Ninja 150 | 151 | - uses: actions/upload-artifact@v5 152 | with: 153 | name: ${{ env.PROJECT_NAME }}-wheels-win_amd64 154 | path: ${{ github.workspace }}/wheelhouse/*.whl 155 | 156 | upload_pypi: 157 | needs: [ build-wheels ] 158 | runs-on: ubuntu-latest 159 | environment: pypi 160 | permissions: 161 | id-token: write 162 | if: (github.event_name == 'release' && github.event.action == 'published') || github.event.inputs.publish == 'true' 163 | 164 | steps: 165 | - uses: actions/download-artifact@v6 166 | with: 167 | pattern: ${{ env.PROJECT_NAME }}-wheels-* 168 | path: dist 169 | merge-multiple: true 170 | 171 | - uses: pypa/gh-action-pypi-publish@release/v1 172 | -------------------------------------------------------------------------------- /include/dxcam/DXCamera.h: -------------------------------------------------------------------------------- 1 | #ifndef DXCAM_CPP_DXCAMERA_H 2 | #define DXCAM_CPP_DXCAMERA_H 3 | 4 | #include 5 | #include 6 | 7 | #include "core/Region.h" 8 | #include "dxcam_export.h" 9 | 10 | namespace DXCam { 11 | 12 | class Device; 13 | class Duplicator; 14 | class Output; 15 | class StageSurface; 16 | 17 | class DXCamera { 18 | public: 19 | DXCamera(Output *output, Device *device, const Region ®ion, 20 | bool region_set_by_user, size_t max_buffer_len = 64); 21 | DXCAM_EXPORT ~DXCamera(); 22 | 23 | // Disallow copy and assign 24 | DXCamera(const DXCamera &) = delete; 25 | DXCamera &operator=(const DXCamera &) = delete; 26 | 27 | /** 28 | * @brief Get the width of the region being captured. 29 | * 30 | * @return The width in pixels. 31 | */ 32 | [[maybe_unused]] DXCAM_EXPORT long get_width() const; 33 | /** 34 | * @brief Get the height of the region being captured. 35 | * 36 | * @return The height in pixels. 37 | */ 38 | [[maybe_unused]] DXCAM_EXPORT long get_height() const; 39 | /** 40 | * @brief Get the rotation angle of the screen being captured. 41 | * 42 | * @return The rotation angle in degrees. 43 | */ 44 | [[maybe_unused]] DXCAM_EXPORT int get_rotation_angle() const; 45 | /** 46 | * @brief Get the region being captured. 47 | * 48 | * @return The region object. 49 | */ 50 | [[maybe_unused]] DXCAM_EXPORT const Region &get_region() const; 51 | /** 52 | * @brief Get the size of the frame buffer in continuous capturing mode. 53 | * 54 | * @return The length of the frame buffer. 55 | */ 56 | [[maybe_unused]] DXCAM_EXPORT size_t get_buffer_len() const; 57 | /** 58 | * @brief Check if the camera is currently capturing. 59 | * 60 | * @return True if capturing, false otherwise. 61 | */ 62 | [[maybe_unused]] DXCAM_EXPORT bool is_capturing() const; 63 | 64 | /** 65 | * @brief Grab the default region instantly. 66 | * 67 | * @return a Mat object, containing the captured image. 68 | */ 69 | [[maybe_unused]] DXCAM_EXPORT cv::Mat grab(); 70 | /** 71 | * @brief Grab the specified region instantly. 72 | * 73 | * @param [in] region The rectangle region to be captured. 74 | * @return a Mat object, containing the captured image. 75 | */ 76 | [[maybe_unused]] DXCAM_EXPORT cv::Mat grab(const Region ®ion); 77 | 78 | /** 79 | * @brief Start capturing the default region. 80 | * 81 | * @param [in] target_fps The target FPS. 82 | * @param [in] video_mode If true, a frame will always be pushed to the 83 | * frame buffer at the target FPS even if there is no a new frame available. 84 | * If false, only new frames will be pushed to the frame buffer. 85 | * @param [in] delay The delay in seconds before capturing starts. 86 | */ 87 | [[maybe_unused]] DXCAM_EXPORT void start(int target_fps = 60, 88 | bool video_mode = false, 89 | int delay = 0); 90 | /** 91 | * @brief Start capturing the specified region. 92 | * 93 | * @param [in] region The rectangle region to be captured. 94 | * @param [in] target_fps The target FPS. 95 | * @param [in] video_mode If true, a frame will always be pushed to the 96 | * frame buffer at the target FPS even if there is no a new frame available. 97 | * If false, only new frames will be pushed to the frame buffer. 98 | * @param [in] delay The delay in seconds before capturing starts. 99 | */ 100 | [[maybe_unused]] DXCAM_EXPORT void start(const Region ®ion, 101 | int target_fps = 60, 102 | bool video_mode = false, 103 | int delay = 0); 104 | 105 | /** 106 | * @brief Stop capturing. 107 | */ 108 | [[maybe_unused]] DXCAM_EXPORT void stop(); 109 | 110 | /** 111 | * @brief Get the latest frame from the frame buffer. 112 | * 113 | * @return a Mat object, containing the latest frame in the frame buffer. 114 | */ 115 | [[maybe_unused]] DXCAM_EXPORT cv::Mat get_latest_frame(); 116 | 117 | /** 118 | * @brief Get the pointers to access the frame buffer, which is a circular 119 | * queue. 120 | * 121 | * If you want to access the whole frame buffer, you should lock 122 | * frame_buffer_all_mutex. If you want to access a single frame, you should 123 | * lock the corresponding mutex in frame_buffer_mutex. 124 | * 125 | * @param [out] frame_buffer The pointer to (the pointer to the first 126 | * element of) the frame buffer array. 127 | * @param [out] frame_buffer_mutex The pointer to (the pointer to the first 128 | * element of) the array of the mutex of single frames. 129 | * @param [out] len The pointer to the length of the frame buffer. 130 | * @param [out] head The pointer to the index of the oldest frame in the 131 | * frame buffer. 132 | * @param [out] tail The pointer to the index of the next frame of the 133 | * latest frame in the frame buffer. 134 | * @param [out] full The pointer to the flag indicating whether the frame 135 | * buffer is full. 136 | * @param [out] frame_buffer_all_mutex The pointer to the mutex of the frame 137 | * buffer. 138 | */ 139 | [[maybe_unused]] DXCAM_EXPORT void get_frame_buffer( 140 | const cv::Mat *const **frame_buffer, 141 | std::mutex *const **frame_buffer_mutex, const size_t **len, 142 | const std::atomic **head, const std::atomic **tail, 143 | const std::atomic **full, std::mutex **frame_buffer_all_mutex); 144 | 145 | private: 146 | void validate_region(const Region ®ion) const; 147 | 148 | void capture(const Region ®ion, int target_fps = 60, 149 | bool video_mode = false) noexcept; 150 | 151 | void on_output_change(); 152 | 153 | void rebuild_frame_buffer(const Region ®ion); 154 | 155 | long width_ = 0; 156 | long height_ = 0; 157 | int rotation_angle_; 158 | Region region_; 159 | bool region_set_by_user_; 160 | bool is_capturing_ = false; 161 | 162 | Output *output_; 163 | Device *device_; 164 | StageSurface *stagesurf_; 165 | Duplicator *duplicator_; 166 | 167 | const size_t buffer_len_; 168 | std::mutex frame_buffer_all_mutex_; 169 | std::mutex *frame_buffer_mutex_ = nullptr; 170 | cv::Mat *frame_buffer_ = nullptr; 171 | std::atomic_int head_ = 0; 172 | std::atomic_int tail_ = 0; 173 | std::atomic_bool full_ = false; 174 | std::atomic_bool frame_available_ = false; 175 | 176 | std::thread thread; 177 | std::atomic_bool stop_capture = false; 178 | }; 179 | 180 | } // namespace DXCam 181 | 182 | #endif // DXCAM_CPP_DXCAMERA_H 183 | -------------------------------------------------------------------------------- /src/DXCamera.cpp: -------------------------------------------------------------------------------- 1 | #include "DXCamera.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "core/Duplicator.h" 9 | #include "core/Output.h" 10 | #include "core/Processor.h" 11 | #include "core/StageSurface.h" 12 | #include "util/HighResTimer.h" 13 | 14 | namespace DXCam { 15 | 16 | DXCamera::DXCamera(Output *const output, Device *const device, 17 | const Region ®ion, const bool region_set_by_user, 18 | const size_t max_buffer_len) 19 | : region_(region), 20 | region_set_by_user_(region_set_by_user), 21 | output_(output), 22 | device_(device), 23 | stagesurf_(new StageSurface(output, device)), 24 | duplicator_(new Duplicator(output, device)), 25 | buffer_len_(max_buffer_len) { 26 | output_->get_resolution(&width_, &height_); 27 | validate_region(region_); 28 | 29 | rotation_angle_ = output->get_rotation_angle(); 30 | } 31 | 32 | DXCamera::~DXCamera() { 33 | stop(); 34 | delete stagesurf_; 35 | delete duplicator_; 36 | } 37 | 38 | long DXCamera::get_width() const { return width_; } 39 | 40 | long DXCamera::get_height() const { return height_; } 41 | 42 | int DXCamera::get_rotation_angle() const { return rotation_angle_; } 43 | 44 | const Region &DXCamera::get_region() const { return region_; } 45 | 46 | size_t DXCamera::get_buffer_len() const { return buffer_len_; } 47 | 48 | bool DXCamera::is_capturing() const { return is_capturing_; } 49 | 50 | void DXCamera::validate_region(const Region ®ion) const { 51 | if (!((0 <= region.left && region.left < region.right && 52 | region.right <= width_) && 53 | (0 <= region.top && region.top < region.bottom && 54 | region.bottom <= height_))) { 55 | throw std::invalid_argument(std::format( 56 | "Invalid region: Region should be in {}x{}", width_, height_)); 57 | } 58 | } 59 | 60 | cv::Mat DXCamera::grab() { return grab(region_); } 61 | 62 | cv::Mat DXCamera::grab(const Region ®ion) { 63 | validate_region(region); 64 | 65 | if (duplicator_->update_frame()) { 66 | if (!duplicator_->updated) { return {}; } 67 | device_->im_context->CopyResource(stagesurf_->texture, 68 | duplicator_->texture); 69 | duplicator_->release_frame(); 70 | const auto rect = stagesurf_->map(); 71 | auto frame = 72 | Processor::process(rect, width_, height_, region, rotation_angle_); 73 | stagesurf_->unmap(); 74 | assert(!frame.empty()); 75 | return frame; 76 | } else { 77 | on_output_change(); 78 | return {}; 79 | } 80 | } 81 | 82 | void DXCamera::start(const int target_fps, const bool video_mode, 83 | const int delay) { 84 | start(region_, target_fps, video_mode, delay); 85 | } 86 | 87 | void DXCamera::start(const Region ®ion, const int target_fps, 88 | const bool video_mode, const int delay) { 89 | if (target_fps <= 0) { 90 | throw std::invalid_argument("Target FPS should be greater than 0"); 91 | } 92 | 93 | if (is_capturing_) { stop(); } 94 | 95 | if (delay != 0) { 96 | std::this_thread::sleep_for(std::chrono::seconds(delay)); 97 | on_output_change(); 98 | } 99 | 100 | validate_region(region); 101 | 102 | is_capturing_ = true; 103 | rebuild_frame_buffer(region); 104 | thread = std::thread([this, region, target_fps, video_mode] { 105 | capture(region, target_fps, video_mode); 106 | }); 107 | } 108 | 109 | void DXCamera::stop() { 110 | if (is_capturing_) { 111 | frame_available_ = true; 112 | frame_available_.notify_all(); 113 | stop_capture = true; 114 | if (thread.joinable()) { thread.join(); } 115 | stop_capture = false; 116 | is_capturing_ = false; 117 | } 118 | delete[] frame_buffer_; 119 | frame_buffer_ = nullptr; 120 | } 121 | 122 | cv::Mat DXCamera::get_latest_frame() { 123 | if (!is_capturing_) throw std::runtime_error("Camera is not capturing"); 124 | 125 | frame_available_.wait(false); 126 | frame_available_ = false; 127 | 128 | const size_t frame_idx = (tail_ - 1 + buffer_len_) % buffer_len_; 129 | std::scoped_lock lock(frame_buffer_mutex_[frame_idx]); 130 | return frame_buffer_[frame_idx]; 131 | } 132 | 133 | void DXCamera::capture(const Region ®ion, const int target_fps, 134 | const bool video_mode) noexcept { 135 | const HighResTimer timer(static_cast(1000.0 / target_fps)); 136 | 137 | // for FPS statistics 138 | int frame_count = 0; 139 | const auto capture_start_time = std::chrono::steady_clock::now(); 140 | 141 | while (!stop_capture) { 142 | timer.wait(); 143 | 144 | cv::Mat frame; 145 | try { 146 | frame = grab(region); 147 | } catch (const std::exception &e) { 148 | std::cerr << e.what() << std::endl; 149 | continue; 150 | } 151 | 152 | if (video_mode || !frame.empty()) { 153 | std::scoped_lock lock_all(frame_buffer_all_mutex_); 154 | 155 | if (video_mode && frame.empty()) { 156 | frame = frame_buffer_[(tail_ - 1 + buffer_len_) % 157 | static_cast(buffer_len_)]; 158 | } 159 | 160 | // The order of the following instructions is important for thread 161 | // safety! 162 | 163 | // Move the head pointer. 164 | // This should be done before writing the frame. 165 | if (full_) { head_ = (head_ + 1) % static_cast(buffer_len_); } 166 | // Now, the frame to be written will be considered outside the range 167 | // of contents of frame buffer. 168 | { 169 | // Lock the mutex of the single frame. 170 | std::scoped_lock lock_frame(frame_buffer_mutex_[tail_]); 171 | frame_buffer_[tail_] = std::move(frame); 172 | } 173 | // Update the "full" flag. 174 | // This should be done before moving the tail pointer; 175 | // otherwise, if the tail pointer is moved but the full flag is not 176 | // updated, the user will get an empty frame buffer. 177 | full_ = (tail_ + 1) % static_cast(buffer_len_) == head_; 178 | // Move the tail pointer. 179 | // This should be done after all the other operations are finished. 180 | // In this case, the new frame is ready to be included in the 181 | // contents of the frame buffer. 182 | tail_ = (tail_ + 1) % static_cast(buffer_len_); 183 | 184 | frame_available_ = true; 185 | frame_available_.notify_all(); 186 | frame_count++; 187 | } 188 | } 189 | 190 | const auto capture_stop_time = std::chrono::steady_clock::now(); 191 | 192 | timer.cancel(); 193 | 194 | // compute FPS statistics 195 | const auto capture_duration = duration_cast( 196 | capture_stop_time - capture_start_time); 197 | const double fps = 198 | static_cast(frame_count) / 199 | static_cast(std::chrono::seconds(capture_duration).count()); 200 | printf("Screen Capture FPS: %lf\n", fps); 201 | } 202 | 203 | void DXCamera::on_output_change() { 204 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 100ms 205 | 206 | output_->update_desc(); 207 | output_->get_resolution(&width_, &height_); 208 | if (region_set_by_user_) { 209 | validate_region(region_); 210 | } else { 211 | region_ = {0, 0, width_, height_}; 212 | } 213 | 214 | if (is_capturing_) { rebuild_frame_buffer(region_); } 215 | 216 | rotation_angle_ = output_->get_rotation_angle(); 217 | 218 | stagesurf_->rebuild(output_, device_); 219 | duplicator_->rebuild(output_, device_); 220 | } 221 | 222 | void DXCamera::rebuild_frame_buffer(const Region ®ion) { 223 | const int region_width = region.get_width(); 224 | const int region_height = region.get_height(); 225 | 226 | // TODO: Check for locked mutex? 227 | 228 | { 229 | std::scoped_lock lock(frame_buffer_all_mutex_); 230 | 231 | delete[] frame_buffer_; 232 | frame_buffer_ = new cv::Mat[buffer_len_]; 233 | for (auto &frame : std::span(frame_buffer_, buffer_len_)) { 234 | frame.create(region_height, region_width, CV_8UC4); 235 | } 236 | delete[] frame_buffer_mutex_; 237 | frame_buffer_mutex_ = new std::mutex[buffer_len_]; 238 | head_ = 0; 239 | tail_ = 0; 240 | full_ = false; 241 | } 242 | } 243 | 244 | void DXCamera::get_frame_buffer(const cv::Mat *const **frame_buffer, 245 | std::mutex *const **frame_buffer_mutex, 246 | const size_t **len, 247 | const std::atomic **head, 248 | const std::atomic **tail, 249 | const std::atomic **full, 250 | std::mutex **frame_buffer_all_mutex) { 251 | if (frame_buffer != nullptr) *frame_buffer = &frame_buffer_; 252 | if (frame_buffer_mutex != nullptr) 253 | *frame_buffer_mutex = &frame_buffer_mutex_; 254 | if (len != nullptr) *len = &buffer_len_; 255 | if (head != nullptr) *head = &head_; 256 | if (tail != nullptr) *tail = &tail_; 257 | if (full != nullptr) *full = &full_; 258 | if (frame_buffer_all_mutex != nullptr) 259 | *frame_buffer_all_mutex = &frame_buffer_all_mutex_; 260 | } 261 | 262 | } // namespace DXCam --------------------------------------------------------------------------------