diff --git a/CMakeLists.txt b/CMakeLists.txt index 43cd0c2..16d6c46 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -282,6 +282,11 @@ else() endif() set(BOOST_LIB Boost::program_options) +if (BUILD_VIDEO2X_CLI) + find_package(Vulkan REQUIRED) + set(VULKAN_LIB Vulkan::Vulkan) +endif() + # Include ExternalProject module include(ExternalProject) @@ -374,7 +379,13 @@ if (BUILD_VIDEO2X_CLI) target_compile_options(video2x PRIVATE $<$:-g -DDEBUG>) # Link the executable with the shared library - target_link_libraries(video2x PRIVATE libvideo2x ${FFMPEG_LIB} ${SPDLOG_LIB} ${BOOST_LIB}) + target_link_libraries(video2x PRIVATE + libvideo2x + ${FFMPEG_LIB} + ${SPDLOG_LIB} + ${BOOST_LIB} + ${VULKAN_LIB} + ) endif() # Define the default installation directories diff --git a/include/libvideo2x/libvideo2x.h b/include/libvideo2x/libvideo2x.h index e831dcf..a11aa4a 100644 --- a/include/libvideo2x/libvideo2x.h +++ b/include/libvideo2x/libvideo2x.h @@ -56,7 +56,6 @@ struct LibplaceboConfig { // Configuration for RealESRGAN filter struct RealESRGANConfig { - int gpuid; bool tta_mode; int scaling_factor; const CharType *model_name; @@ -100,6 +99,7 @@ struct VideoProcessingContext { * @param[in] out_fname Path to the output video file * @param[in] log_level Log level * @param[in] benchmark Flag to enable benchmarking mode + * @param[in] vk_device_index Vulkan device index * @param[in] hw_type Hardware device type * @param[in] filter_config Filter configurations * @param[in] encoder_config Encoder configurations @@ -111,6 +111,7 @@ LIBVIDEO2X_API int process_video( const CharType *out_fname, enum Libvideo2xLogLevel log_level, bool benchmark, + uint32_t vk_device_index, enum AVHWDeviceType hw_device_type, const struct FilterConfig *filter_config, struct EncoderConfig *encoder_config, diff --git a/src/libvideo2x.cpp b/src/libvideo2x.cpp index 93e000c..0d55d45 100644 --- a/src/libvideo2x.cpp +++ b/src/libvideo2x.cpp @@ -92,7 +92,7 @@ static int process_frames( AVPacket *packet = av_packet_alloc(); if (packet == nullptr) { - spdlog::error("Could not allocate AVPacket"); + spdlog::critical("Could not allocate AVPacket"); av_frame_free(&frame); return AVERROR(ENOMEM); } @@ -118,7 +118,7 @@ static int process_frames( break; } av_strerror(ret, errbuf, sizeof(errbuf)); - spdlog::error("Error reading packet: {}", errbuf); + spdlog::critical("Error reading packet: {}", errbuf); cleanup(); return ret; } @@ -127,7 +127,7 @@ static int process_frames( ret = avcodec_send_packet(dec_ctx, packet); if (ret < 0) { av_strerror(ret, errbuf, sizeof(errbuf)); - spdlog::error("Error sending packet to decoder: {}", errbuf); + spdlog::critical("Error sending packet to decoder: {}", errbuf); av_packet_unref(packet); cleanup(); return ret; @@ -145,7 +145,7 @@ static int process_frames( break; } else if (ret < 0) { av_strerror(ret, errbuf, sizeof(errbuf)); - spdlog::error("Error decoding video frame: {}", errbuf); + spdlog::critical("Error decoding video frame: {}", errbuf); av_packet_unref(packet); cleanup(); return ret; @@ -164,7 +164,7 @@ static int process_frames( ret = write_frame(processed_frame, enc_ctx, ofmt_ctx, vstream_idx); if (ret < 0) { av_strerror(ret, errbuf, sizeof(errbuf)); - spdlog::error("Error encoding/writing frame: {}", errbuf); + spdlog::critical("Error encoding/writing frame: {}", errbuf); av_frame_free(&processed_frame); av_packet_unref(packet); cleanup(); @@ -204,7 +204,7 @@ static int process_frames( ret = filter->flush(flushed_frames); if (ret < 0) { av_strerror(ret, errbuf, sizeof(errbuf)); - spdlog::error("Error flushing filter: {}", errbuf); + spdlog::critical("Error flushing filter: {}", errbuf); cleanup(); return ret; } @@ -214,7 +214,7 @@ static int process_frames( ret = write_frame(flushed_frame, enc_ctx, ofmt_ctx, vstream_idx); if (ret < 0) { av_strerror(ret, errbuf, sizeof(errbuf)); - spdlog::error("Error encoding/writing flushed frame: {}", errbuf); + spdlog::critical("Error encoding/writing flushed frame: {}", errbuf); av_frame_free(&flushed_frame); flushed_frame = nullptr; cleanup(); @@ -229,7 +229,7 @@ static int process_frames( ret = flush_encoder(enc_ctx, ofmt_ctx); if (ret < 0) { av_strerror(ret, errbuf, sizeof(errbuf)); - spdlog::error("Error flushing encoder: {}", errbuf); + spdlog::critical("Error flushing encoder: {}", errbuf); cleanup(); return ret; } @@ -243,6 +243,7 @@ extern "C" int process_video( const CharType *out_fname, Libvideo2xLogLevel log_level, bool benchmark, + uint32_t vk_device_index, AVHWDeviceType hw_type, const FilterConfig *filter_config, EncoderConfig *encoder_config, @@ -340,7 +341,7 @@ extern "C" int process_video( ret = av_hwdevice_ctx_create(&hw_ctx, hw_type, NULL, NULL, 0); if (ret < 0) { av_strerror(ret, errbuf, sizeof(errbuf)); - spdlog::error("Error initializing hardware device context: {}", errbuf); + spdlog::critical("Error initializing hardware device context: {}", errbuf); cleanup(); return ret; } @@ -350,7 +351,7 @@ extern "C" int process_video( ret = init_decoder(hw_type, hw_ctx, in_fpath, &ifmt_ctx, &dec_ctx, &vstream_idx); if (ret < 0) { av_strerror(ret, errbuf, sizeof(errbuf)); - spdlog::error("Failed to initialize decoder: {}", errbuf); + spdlog::critical("Failed to initialize decoder: {}", errbuf); cleanup(); return ret; } @@ -367,7 +368,7 @@ extern "C" int process_video( output_height = dec_ctx->height * filter_config->config.realesrgan.scaling_factor; break; default: - spdlog::error("Unknown filter type"); + spdlog::critical("Unknown filter type"); cleanup(); return -1; } @@ -389,7 +390,7 @@ extern "C" int process_video( ); if (ret < 0) { av_strerror(ret, errbuf, sizeof(errbuf)); - spdlog::error("Failed to initialize encoder: {}", errbuf); + spdlog::critical("Failed to initialize encoder: {}", errbuf); cleanup(); return ret; } @@ -398,7 +399,7 @@ extern "C" int process_video( ret = avformat_write_header(ofmt_ctx, NULL); if (ret < 0) { av_strerror(ret, errbuf, sizeof(errbuf)); - spdlog::error("Error occurred when opening output file: {}", errbuf); + spdlog::critical("Error occurred when opening output file: {}", errbuf); cleanup(); return ret; } @@ -407,7 +408,7 @@ extern "C" int process_video( if (filter_config->filter_type == FILTER_LIBPLACEBO) { const auto &config = filter_config->config.libplacebo; if (!config.shader_path) { - spdlog::error("Shader path must be provided for the libplacebo filter"); + spdlog::critical("Shader path must be provided for the libplacebo filter"); cleanup(); return -1; } @@ -417,22 +418,25 @@ extern "C" int process_video( } else if (filter_config->filter_type == FILTER_REALESRGAN) { const auto &config = filter_config->config.realesrgan; if (!config.model_name) { - spdlog::error("Model name must be provided for the RealESRGAN filter"); + spdlog::critical("Model name must be provided for the RealESRGAN filter"); cleanup(); return -1; } filter = new RealesrganFilter{ - config.gpuid, config.tta_mode, config.scaling_factor, config.model_name + static_cast(vk_device_index), + config.tta_mode, + config.scaling_factor, + config.model_name }; } else { - spdlog::error("Unknown filter type"); + spdlog::critical("Unknown filter type"); cleanup(); return -1; } // Check if the filter instance was created successfully if (filter == nullptr) { - spdlog::error("Failed to create filter instance"); + spdlog::critical("Failed to create filter instance"); cleanup(); return -1; } @@ -440,7 +444,7 @@ extern "C" int process_video( // Initialize the filter ret = filter->init(dec_ctx, enc_ctx, hw_ctx); if (ret < 0) { - spdlog::error("Failed to initialize filter"); + spdlog::critical("Failed to initialize filter"); cleanup(); return ret; } @@ -460,7 +464,7 @@ extern "C" int process_video( ); if (ret < 0) { av_strerror(ret, errbuf, sizeof(errbuf)); - spdlog::error("Error processing frames: {}", errbuf); + spdlog::critical("Error processing frames: {}", errbuf); cleanup(); return ret; } @@ -473,7 +477,7 @@ extern "C" int process_video( if (ret < 0 && ret != AVERROR_EOF) { av_strerror(ret, errbuf, sizeof(errbuf)); - spdlog::error("Error occurred: {}", errbuf); + spdlog::critical("Error occurred: {}", errbuf); return ret; } return 0; diff --git a/src/realesrgan_filter.cpp b/src/realesrgan_filter.cpp index 4289242..79f6f2d 100644 --- a/src/realesrgan_filter.cpp +++ b/src/realesrgan_filter.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include diff --git a/src/video2x.cpp b/src/video2x.cpp index 881e22f..c7f3078 100644 --- a/src/video2x.cpp +++ b/src/video2x.cpp @@ -33,6 +33,7 @@ extern "C" { } #include +#include #ifdef _WIN32 #define BOOST_PROGRAM_OPTIONS_WCHAR_T @@ -49,17 +50,22 @@ namespace po = boost::program_options; // Indicate if a newline needs to be printed before the next output std::atomic newline_required = false; +// Mutex for synchronizing access to VideoProcessingContext +std::mutex proc_ctx_mutex; + // Structure to hold parsed arguments struct Arguments { + StringType loglevel = STR("info"); + bool noprogress = false; + // General options std::filesystem::path in_fname; std::filesystem::path out_fname; StringType filter_type; + uint32_t gpuid = 0; StringType hwaccel = STR("none"); bool nocopystreams = false; bool benchmark = false; - StringType loglevel = STR("info"); - bool noprogress = false; // Encoder options StringType codec = STR("libx264"); @@ -74,7 +80,6 @@ struct Arguments { int out_height = 0; // RealESRGAN options - int gpuid = 0; StringType model_name; int scaling_factor = 0; }; @@ -161,8 +166,112 @@ enum Libvideo2xLogLevel parse_log_level(const StringType &level_name) { } } -// Mutex for synchronizing access to VideoProcessingContext -std::mutex proc_ctx_mutex; +int list_gpus() { + // Create a Vulkan instance + VkInstance instance; + VkInstanceCreateInfo create_info{}; + create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + if (vkCreateInstance(&create_info, nullptr, &instance) != VK_SUCCESS) { + spdlog::critical("Failed to create Vulkan instance."); + return -1; + } + + // Enumerate physical devices + uint32_t device_count = 0; + VkResult result = vkEnumeratePhysicalDevices(instance, &device_count, nullptr); + if (result != VK_SUCCESS) { + spdlog::critical("Failed to enumerate Vulkan physical devices."); + vkDestroyInstance(instance, nullptr); + return -1; + } + + // Check if any devices are found + if (device_count == 0) { + spdlog::critical("No Vulkan physical devices found."); + vkDestroyInstance(instance, nullptr); + return -1; + } + + // Get physical device properties + std::vector physical_devices(device_count); + result = vkEnumeratePhysicalDevices(instance, &device_count, physical_devices.data()); + if (result != VK_SUCCESS) { + spdlog::critical("Failed to enumerate Vulkan physical devices."); + vkDestroyInstance(instance, nullptr); + return -1; + } + + // List GPU information + for (uint32_t i = 0; i < device_count; i++) { + VkPhysicalDevice device = physical_devices[i]; + VkPhysicalDeviceProperties device_properties; + vkGetPhysicalDeviceProperties(device, &device_properties); + + // Print GPU ID and name + std::cout << i << ". " << device_properties.deviceName << std::endl; + std::cout << "\tType: "; + switch (device_properties.deviceType) { + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: + std::cout << "Integrated GPU"; + break; + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: + std::cout << "Discrete GPU"; + break; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: + std::cout << "Virtual GPU"; + break; + case VK_PHYSICAL_DEVICE_TYPE_CPU: + std::cout << "CPU"; + break; + default: + std::cout << "Unknown"; + break; + } + std::cout << std::endl; + + // Print Vulkan API version + std::cout << "\tVulkan API Version: " << VK_VERSION_MAJOR(device_properties.apiVersion) + << "." << VK_VERSION_MINOR(device_properties.apiVersion) << "." + << VK_VERSION_PATCH(device_properties.apiVersion) << std::endl; + + // Print driver version + std::cout << "\tDriver Version: " << VK_VERSION_MAJOR(device_properties.driverVersion) + << "." << VK_VERSION_MINOR(device_properties.driverVersion) << "." + << VK_VERSION_PATCH(device_properties.driverVersion) << std::endl; + } + + // Clean up Vulkan instance + vkDestroyInstance(instance, nullptr); + return 0; +} + +int is_valid_gpu_id(uint32_t gpu_id) { + // Create a Vulkan instance + VkInstance instance; + VkInstanceCreateInfo create_info{}; + create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + if (vkCreateInstance(&create_info, nullptr, &instance) != VK_SUCCESS) { + spdlog::error("Failed to create Vulkan instance."); + return -1; + } + + // Enumerate physical devices + uint32_t device_count = 0; + VkResult result = vkEnumeratePhysicalDevices(instance, &device_count, nullptr); + if (result != VK_SUCCESS) { + spdlog::error("Failed to enumerate Vulkan physical devices."); + vkDestroyInstance(instance, nullptr); + return -1; + } + + // Clean up Vulkan instance + vkDestroyInstance(instance, nullptr); + + if (gpu_id >= device_count) { + return 0; + } + return 1; +} // Wrapper function for video processing thread void process_video_thread( @@ -194,6 +303,7 @@ void process_video_thread( out_fname, log_level, arguments->benchmark, + arguments->gpuid, hw_device_type, filter_config, encoder_config, @@ -224,11 +334,13 @@ int main(int argc, char **argv) { ("version,v", "Print program version") ("loglevel", PO_STR_VALUE(&arguments.loglevel)->default_value(STR("info"), "info"), "Set log level (trace, debug, info, warn, error, critical, none)") ("noprogress", po::bool_switch(&arguments.noprogress), "Do not display the progress bar") + ("listgpus", "List the available GPUs") // General Processing Options ("input,i", PO_STR_VALUE(), "Input video file path") ("output,o", PO_STR_VALUE(), "Output video file path") ("filter,f", PO_STR_VALUE(&arguments.filter_type), "Filter to use: 'libplacebo' or 'realesrgan'") + ("gpuid,g", po::value(&arguments.gpuid)->default_value(0), "Vulkan GPU ID (default: 0)") ("hwaccel,a", PO_STR_VALUE(&arguments.hwaccel)->default_value(STR("none"), "none"), "Hardware acceleration method (default: none)") ("nocopystreams", po::bool_switch(&arguments.nocopystreams), "Do not copy audio and subtitle streams") ("benchmark", po::bool_switch(&arguments.benchmark), "Discard processed frames and calculate average FPS") @@ -246,7 +358,6 @@ int main(int argc, char **argv) { ("height,h", po::value(&arguments.out_height), "Output height") // RealESRGAN options - ("gpuid,g", po::value(&arguments.gpuid)->default_value(0), "Vulkan GPU ID (default: 0)") ("model,m", PO_STR_VALUE(&arguments.model_name), "Name of the model to use") ("scale,r", po::value(&arguments.scaling_factor), "Scaling factor (2, 3, or 4)") ; @@ -274,23 +385,27 @@ int main(int argc, char **argv) { return 0; } + if (vm.count("listgpus")) { + return list_gpus(); + } + // Assign positional arguments if (vm.count("input")) { arguments.in_fname = std::filesystem::path(vm["input"].as()); } else { - spdlog::error("Error: Input file path is required."); + spdlog::critical("Input file path is required."); return 1; } if (vm.count("output")) { arguments.out_fname = std::filesystem::path(vm["output"].as()); } else if (!arguments.benchmark) { - spdlog::error("Error: Output file path is required."); + spdlog::critical("Output file path is required."); return 1; } if (!vm.count("filter")) { - spdlog::error("Error: Filter type is required (libplacebo or realesrgan)."); + spdlog::critical("Filter type is required (libplacebo or realesrgan)."); return 1; } @@ -300,18 +415,18 @@ int main(int argc, char **argv) { if (vm.count("model")) { if (!is_valid_realesrgan_model(vm["model"].as())) { - spdlog::error( - "Error: Invalid model specified. Must be 'realesrgan-plus', " + spdlog::critical( + "Invalid model specified. Must be 'realesrgan-plus', " "'realesrgan-plus-anime', or 'realesr-animevideov3'." ); return 1; } } } catch (const po::error &e) { - spdlog::error("Error parsing options: {}", e.what()); + spdlog::critical("Error parsing options: {}", e.what()); return 1; } catch (const std::exception &e) { - spdlog::error("Unexpected exception caught while parsing options: {}", e.what()); + spdlog::critical("Unexpected exception caught while parsing options: {}", e.what()); return 1; } @@ -319,45 +434,52 @@ int main(int argc, char **argv) { if (arguments.filter_type == STR("libplacebo")) { if (arguments.shader_path.empty() || arguments.out_width == 0 || arguments.out_height == 0) { - spdlog::error( - "Error: For libplacebo, shader name/path (-s), width (-w), " + spdlog::critical( + "For libplacebo, shader name/path (-s), width (-w), " "and height (-h) are required." ); return 1; } } else if (arguments.filter_type == STR("realesrgan")) { if (arguments.scaling_factor == 0 || arguments.model_name.empty()) { - spdlog::error("Error: For realesrgan, scaling factor (-r) and model (-m) are required." - ); + spdlog::critical("For realesrgan, scaling factor (-r) and model (-m) are required."); return 1; } if (arguments.scaling_factor != 2 && arguments.scaling_factor != 3 && arguments.scaling_factor != 4) { - spdlog::error("Error: Scaling factor must be 2, 3, or 4."); + spdlog::critical("Scaling factor must be 2, 3, or 4."); return 1; } } else { - spdlog::error("Error: Invalid filter type specified. Must be 'libplacebo' or 'realesrgan'." - ); + spdlog::critical("Invalid filter type specified. Must be 'libplacebo' or 'realesrgan'."); + return 1; + } + + // Validate GPU ID + int gpu_status = is_valid_gpu_id(arguments.gpuid); + if (gpu_status < 0) { + spdlog::warn("Unable to validate GPU ID."); + } else if (arguments.gpuid > 0 && gpu_status == 0) { + spdlog::critical("Invalid GPU ID specified."); return 1; } // Validate bitrate if (arguments.bitrate < 0) { - spdlog::error("Error: Invalid bitrate specified."); + spdlog::critical("Invalid bitrate specified."); return 1; } // Validate CRF if (arguments.crf < 0.0f || arguments.crf > 51.0f) { - spdlog::error("Error: CRF must be between 0 and 51."); + spdlog::critical("CRF must be between 0 and 51."); return 1; } // Parse codec to AVCodec const AVCodec *codec = avcodec_find_encoder_by_name(wstring_to_utf8(arguments.codec).c_str()); if (!codec) { - spdlog::error("Error: Codec '{}' not found.", wstring_to_utf8(arguments.codec)); + spdlog::critical("Codec '{}' not found.", wstring_to_utf8(arguments.codec)); return 1; } @@ -366,7 +488,7 @@ int main(int argc, char **argv) { if (!arguments.pix_fmt.empty()) { pix_fmt = av_get_pix_fmt(wstring_to_utf8(arguments.pix_fmt).c_str()); if (pix_fmt == AV_PIX_FMT_NONE) { - spdlog::error("Error: Invalid pixel format '{}'.", wstring_to_utf8(arguments.pix_fmt)); + spdlog::critical("Invalid pixel format '{}'.", wstring_to_utf8(arguments.pix_fmt)); return 1; } } @@ -400,6 +522,10 @@ int main(int argc, char **argv) { break; } + // Print program version and processing information + spdlog::info("Video2X version {}", LIBVIDEO2X_VERSION_STRING); + spdlog::info("Processing file: {}", arguments.in_fname.u8string()); + #ifdef _WIN32 std::wstring shader_path_str = arguments.shader_path.wstring(); #else @@ -415,7 +541,6 @@ int main(int argc, char **argv) { filter_config.config.libplacebo.shader_path = shader_path_str.c_str(); } else if (arguments.filter_type == STR("realesrgan")) { filter_config.filter_type = FILTER_REALESRGAN; - filter_config.config.realesrgan.gpuid = arguments.gpuid; filter_config.config.realesrgan.tta_mode = false; filter_config.config.realesrgan.scaling_factor = arguments.scaling_factor; filter_config.config.realesrgan.model_name = arguments.model_name.c_str(); @@ -439,8 +564,8 @@ int main(int argc, char **argv) { if (arguments.hwaccel != STR("none")) { hw_device_type = av_hwdevice_find_type_by_name(wstring_to_utf8(arguments.hwaccel).c_str()); if (hw_device_type == AV_HWDEVICE_TYPE_NONE) { - spdlog::error( - "Error: Invalid hardware device type '{}'.", wstring_to_utf8(arguments.hwaccel) + spdlog::critical( + "Invalid hardware device type '{}'.", wstring_to_utf8(arguments.hwaccel) ); return 1; }