diff --git a/.clang-format b/.clang-format index 9f868d75..97bdef1e 100644 --- a/.clang-format +++ b/.clang-format @@ -7,7 +7,7 @@ BasedOnStyle: LLVM AccessModifierOffset: -2 AlignAfterOpenBracket: DontAlign -AlignConsecutiveAssignments: Consecutive +AlignConsecutiveAssignments: false AlignOperands: Align AllowAllArgumentsOnNextLine: false AllowAllConstructorInitializersOnNextLine: false @@ -18,8 +18,9 @@ AllowShortFunctionsOnASingleLine: All AllowShortIfStatementsOnASingleLine: WithoutElse AllowShortLambdasOnASingleLine: All AllowShortLoopsOnASingleLine: true -AlwaysBreakAfterReturnType: None -AlwaysBreakTemplateDeclarations: Yes +AlignTrailingComments: false +AlwaysBreakAfterReturnType: All +AlwaysBreakTemplateDeclarations: MultiLine BreakBeforeBraces: Custom BraceWrapping: AfterCaseLabel: false @@ -37,32 +38,32 @@ BraceWrapping: SplitEmptyRecord: true BreakBeforeBinaryOperators: None BreakBeforeTernaryOperators: false -BreakConstructorInitializers: BeforeColon -BreakInheritanceList: BeforeColon +BreakConstructorInitializers: AfterColon +BreakInheritanceList: AfterColon ColumnLimit: 0 CompactNamespaces: false ContinuationIndentWidth: 2 -IndentCaseLabels: false -IndentPPDirectives: None +IndentCaseLabels: true +IndentPPDirectives: BeforeHash IndentWidth: 2 -KeepEmptyLinesAtTheStartOfBlocks: true -MaxEmptyLinesToKeep: 2 -NamespaceIndentation: None -ObjCSpaceAfterProperty: false +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +ObjCSpaceAfterProperty: true ObjCSpaceBeforeProtocolList: true PointerAlignment: Right ReflowComments: false -SpaceAfterCStyleCast: false +SpaceAfterCStyleCast: true SpaceAfterLogicalNot: false -SpaceAfterTemplateKeyword: false +SpaceAfterTemplateKeyword: true SpaceBeforeAssignmentOperators: true SpaceBeforeCpp11BracedList: true -SpaceBeforeCtorInitializerColon: true -SpaceBeforeInheritanceColon: true -SpaceBeforeParens: Never +SpaceBeforeCtorInitializerColon: false +SpaceBeforeInheritanceColon: false +SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: true SpaceInEmptyParentheses: false -SpacesBeforeTrailingComments: 1 +SpacesBeforeTrailingComments: 2 SpacesInAngles: Never SpacesInCStyleCastParentheses: false SpacesInContainerLiterals: false diff --git a/scripts/update_clang_format.py b/scripts/update_clang_format.py new file mode 100644 index 00000000..2624e794 --- /dev/null +++ b/scripts/update_clang_format.py @@ -0,0 +1,40 @@ +# standard imports +import os +import subprocess + +# variables +directories = [ + 'src', + 'tools', + os.path.join('third-party', 'glad'), + os.path.join('third-party', 'nvfbc'), + os.path.join('third-party', 'wayland-protocols') +] +file_types = [ + 'cpp', + 'h', + 'm', + 'mm' +] + + +def clang_format(file: str): + print(f'Formatting {file} ...') + subprocess.run(['clang-format', '-i', file]) + + +def main(): + """ + Main entry point. + """ + # walk the directories + for directory in directories: + for root, dirs, files in os.walk(directory): + for file in files: + file_path = os.path.join(root, file) + if os.path.isfile(file_path) and file.rsplit('.')[-1] in file_types: + clang_format(file=file_path) + + +if __name__ == '__main__': + main() diff --git a/src/audio.cpp b/src/audio.cpp index 153da686..e926e013 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -11,271 +11,279 @@ #include "utility.h" namespace audio { -using namespace std::literals; -using opus_t = util::safe_ptr; -using sample_queue_t = std::shared_ptr>>; + using namespace std::literals; + using opus_t = util::safe_ptr; + using sample_queue_t = std::shared_ptr>>; -struct audio_ctx_t { - // We want to change the sink for the first stream only - std::unique_ptr sink_flag; + struct audio_ctx_t { + // We want to change the sink for the first stream only + std::unique_ptr sink_flag; - std::unique_ptr control; + std::unique_ptr control; - bool restore_sink; - platf::sink_t sink; -}; + bool restore_sink; + platf::sink_t sink; + }; -static int start_audio_control(audio_ctx_t &ctx); -static void stop_audio_control(audio_ctx_t &); + static int + start_audio_control(audio_ctx_t &ctx); + static void + stop_audio_control(audio_ctx_t &); -int map_stream(int channels, bool quality); + int + map_stream(int channels, bool quality); -constexpr auto SAMPLE_RATE = 48000; + constexpr auto SAMPLE_RATE = 48000; -opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] { - { - SAMPLE_RATE, - 2, - 1, - 1, - platf::speaker::map_stereo, - 96000, - }, - { - SAMPLE_RATE, - 2, - 1, - 1, - platf::speaker::map_stereo, - 512000, - }, - { - SAMPLE_RATE, - 6, - 4, - 2, - platf::speaker::map_surround51, - 256000, - }, - { - SAMPLE_RATE, - 6, - 6, - 0, - platf::speaker::map_surround51, - 1536000, - }, - { - SAMPLE_RATE, - 8, - 5, - 3, - platf::speaker::map_surround71, - 450000, - }, - { - SAMPLE_RATE, - 8, - 8, - 0, - platf::speaker::map_surround71, - 2048000, - }, -}; + opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] { + { + SAMPLE_RATE, + 2, + 1, + 1, + platf::speaker::map_stereo, + 96000, + }, + { + SAMPLE_RATE, + 2, + 1, + 1, + platf::speaker::map_stereo, + 512000, + }, + { + SAMPLE_RATE, + 6, + 4, + 2, + platf::speaker::map_surround51, + 256000, + }, + { + SAMPLE_RATE, + 6, + 6, + 0, + platf::speaker::map_surround51, + 1536000, + }, + { + SAMPLE_RATE, + 8, + 5, + 3, + platf::speaker::map_surround71, + 450000, + }, + { + SAMPLE_RATE, + 8, + 8, + 0, + platf::speaker::map_surround71, + 2048000, + }, + }; -auto control_shared = safe::make_shared(start_audio_control, stop_audio_control); + auto control_shared = safe::make_shared(start_audio_control, stop_audio_control); -void encodeThread(sample_queue_t samples, config_t config, void *channel_data) { - auto packets = mail::man->queue(mail::audio_packets); - auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])]; + void + encodeThread(sample_queue_t samples, config_t config, void *channel_data) { + auto packets = mail::man->queue(mail::audio_packets); + auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])]; - // Encoding takes place on this thread - platf::adjust_thread_priority(platf::thread_priority_e::high); + // Encoding takes place on this thread + platf::adjust_thread_priority(platf::thread_priority_e::high); - opus_t opus { opus_multistream_encoder_create( - stream->sampleRate, - stream->channelCount, - stream->streams, - stream->coupledStreams, - stream->mapping, - OPUS_APPLICATION_RESTRICTED_LOWDELAY, - nullptr) }; + opus_t opus { opus_multistream_encoder_create( + stream->sampleRate, + stream->channelCount, + stream->streams, + stream->coupledStreams, + stream->mapping, + OPUS_APPLICATION_RESTRICTED_LOWDELAY, + nullptr) }; - opus_multistream_encoder_ctl(opus.get(), OPUS_SET_BITRATE(stream->bitrate)); - opus_multistream_encoder_ctl(opus.get(), OPUS_SET_VBR(0)); + opus_multistream_encoder_ctl(opus.get(), OPUS_SET_BITRATE(stream->bitrate)); + opus_multistream_encoder_ctl(opus.get(), OPUS_SET_VBR(0)); - auto frame_size = config.packetDuration * stream->sampleRate / 1000; - while(auto sample = samples->pop()) { - buffer_t packet { 1400 }; + auto frame_size = config.packetDuration * stream->sampleRate / 1000; + while (auto sample = samples->pop()) { + buffer_t packet { 1400 }; - int bytes = opus_multistream_encode(opus.get(), sample->data(), frame_size, std::begin(packet), packet.size()); - if(bytes < 0) { - BOOST_LOG(error) << "Couldn't encode audio: "sv << opus_strerror(bytes); - packets->stop(); - - return; - } - - packet.fake_resize(bytes); - packets->raise(channel_data, std::move(packet)); - } -} - -void capture(safe::mail_t mail, config_t config, void *channel_data) { - auto shutdown_event = mail->event(mail::shutdown); - auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])]; - - auto ref = control_shared.ref(); - if(!ref) { - return; - } - - auto &control = ref->control; - if(!control) { - shutdown_event->view(); - - return; - } - - // Order of priority: - // 1. Config - // 2. Virtual if available - // 3. Host - std::string *sink = &ref->sink.host; - if(!config::audio.sink.empty()) { - sink = &config::audio.sink; - } - else if(ref->sink.null) { - auto &null = *ref->sink.null; - switch(stream->channelCount) { - case 2: - sink = &null.stereo; - break; - case 6: - sink = &null.surround51; - break; - case 8: - sink = &null.surround71; - break; - } - } - - // Only the first to start a session may change the default sink - if(!ref->sink_flag->exchange(true, std::memory_order_acquire)) { - ref->restore_sink = !config.flags[config_t::HOST_AUDIO]; - - // If the sink is empty (Host has no sink!), definately switch to the virtual. - if(ref->sink.host.empty()) { - if(control->set_sink(*sink)) { - return; - } - } - // If the client requests audio on the host, don't change the default sink - else if(!config.flags[config_t::HOST_AUDIO] && control->set_sink(*sink)) { - return; - } - } - - // Capture takes place on this thread - platf::adjust_thread_priority(platf::thread_priority_e::critical); - - auto samples = std::make_shared(30); - std::thread thread { encodeThread, samples, config, channel_data }; - - auto fg = util::fail_guard([&]() { - samples->stop(); - thread.join(); - - shutdown_event->view(); - }); - - auto frame_size = config.packetDuration * stream->sampleRate / 1000; - int samples_per_frame = frame_size * stream->channelCount; - - auto mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size); - if(!mic) { - BOOST_LOG(error) << "Couldn't create audio input"sv; - - return; - } - - while(!shutdown_event->peek()) { - std::vector sample_buffer; - sample_buffer.resize(samples_per_frame); - - auto status = mic->sample(sample_buffer); - switch(status) { - case platf::capture_e::ok: - break; - case platf::capture_e::timeout: - continue; - case platf::capture_e::reinit: - mic.reset(); - mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size); - if(!mic) { - BOOST_LOG(error) << "Couldn't re-initialize audio input"sv; + int bytes = opus_multistream_encode(opus.get(), sample->data(), frame_size, std::begin(packet), packet.size()); + if (bytes < 0) { + BOOST_LOG(error) << "Couldn't encode audio: "sv << opus_strerror(bytes); + packets->stop(); return; } - return; - default: + + packet.fake_resize(bytes); + packets->raise(channel_data, std::move(packet)); + } + } + + void + capture(safe::mail_t mail, config_t config, void *channel_data) { + auto shutdown_event = mail->event(mail::shutdown); + auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])]; + + auto ref = control_shared.ref(); + if (!ref) { return; } - samples->raise(std::move(sample_buffer)); + auto &control = ref->control; + if (!control) { + shutdown_event->view(); + + return; + } + + // Order of priority: + // 1. Config + // 2. Virtual if available + // 3. Host + std::string *sink = &ref->sink.host; + if (!config::audio.sink.empty()) { + sink = &config::audio.sink; + } + else if (ref->sink.null) { + auto &null = *ref->sink.null; + switch (stream->channelCount) { + case 2: + sink = &null.stereo; + break; + case 6: + sink = &null.surround51; + break; + case 8: + sink = &null.surround71; + break; + } + } + + // Only the first to start a session may change the default sink + if (!ref->sink_flag->exchange(true, std::memory_order_acquire)) { + ref->restore_sink = !config.flags[config_t::HOST_AUDIO]; + + // If the sink is empty (Host has no sink!), definately switch to the virtual. + if (ref->sink.host.empty()) { + if (control->set_sink(*sink)) { + return; + } + } + // If the client requests audio on the host, don't change the default sink + else if (!config.flags[config_t::HOST_AUDIO] && control->set_sink(*sink)) { + return; + } + } + + // Capture takes place on this thread + platf::adjust_thread_priority(platf::thread_priority_e::critical); + + auto samples = std::make_shared(30); + std::thread thread { encodeThread, samples, config, channel_data }; + + auto fg = util::fail_guard([&]() { + samples->stop(); + thread.join(); + + shutdown_event->view(); + }); + + auto frame_size = config.packetDuration * stream->sampleRate / 1000; + int samples_per_frame = frame_size * stream->channelCount; + + auto mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size); + if (!mic) { + BOOST_LOG(error) << "Couldn't create audio input"sv; + + return; + } + + while (!shutdown_event->peek()) { + std::vector sample_buffer; + sample_buffer.resize(samples_per_frame); + + auto status = mic->sample(sample_buffer); + switch (status) { + case platf::capture_e::ok: + break; + case platf::capture_e::timeout: + continue; + case platf::capture_e::reinit: + mic.reset(); + mic = control->microphone(stream->mapping, stream->channelCount, stream->sampleRate, frame_size); + if (!mic) { + BOOST_LOG(error) << "Couldn't re-initialize audio input"sv; + + return; + } + return; + default: + return; + } + + samples->raise(std::move(sample_buffer)); + } } -} -int map_stream(int channels, bool quality) { - int shift = quality ? 1 : 0; - switch(channels) { - case 2: - return STEREO + shift; - case 6: - return SURROUND51 + shift; - case 8: - return SURROUND71 + shift; + int + map_stream(int channels, bool quality) { + int shift = quality ? 1 : 0; + switch (channels) { + case 2: + return STEREO + shift; + case 6: + return SURROUND51 + shift; + case 8: + return SURROUND71 + shift; + } + return STEREO; } - return STEREO; -} -int start_audio_control(audio_ctx_t &ctx) { - auto fg = util::fail_guard([]() { - BOOST_LOG(warning) << "There will be no audio"sv; - }); + int + start_audio_control(audio_ctx_t &ctx) { + auto fg = util::fail_guard([]() { + BOOST_LOG(warning) << "There will be no audio"sv; + }); - ctx.sink_flag = std::make_unique(false); + ctx.sink_flag = std::make_unique(false); - // The default sink has not been replaced yet. - ctx.restore_sink = false; + // The default sink has not been replaced yet. + ctx.restore_sink = false; - if(!(ctx.control = platf::audio_control())) { + if (!(ctx.control = platf::audio_control())) { + return 0; + } + + auto sink = ctx.control->sink_info(); + if (!sink) { + // Let the calling code know it failed + ctx.control.reset(); + return 0; + } + + ctx.sink = std::move(*sink); + + fg.disable(); return 0; } - auto sink = ctx.control->sink_info(); - if(!sink) { - // Let the calling code know it failed - ctx.control.reset(); - return 0; + void + stop_audio_control(audio_ctx_t &ctx) { + // restore audio-sink if applicable + if (!ctx.restore_sink) { + return; + } + + const std::string &sink = config::audio.sink.empty() ? ctx.sink.host : config::audio.sink; + if (!sink.empty()) { + // Best effort, it's allowed to fail + ctx.control->set_sink(sink); + } } - - ctx.sink = std::move(*sink); - - fg.disable(); - return 0; -} - -void stop_audio_control(audio_ctx_t &ctx) { - // restore audio-sink if applicable - if(!ctx.restore_sink) { - return; - } - - const std::string &sink = config::audio.sink.empty() ? ctx.sink.host : config::audio.sink; - if(!sink.empty()) { - // Best effort, it's allowed to fail - ctx.control->set_sink(sink); - } -} -} // namespace audio +} // namespace audio diff --git a/src/audio.h b/src/audio.h index 17d3ebb4..09a99b03 100644 --- a/src/audio.h +++ b/src/audio.h @@ -4,44 +4,45 @@ #include "thread_safe.h" #include "utility.h" namespace audio { -enum stream_config_e : int { - STEREO, - HIGH_STEREO, - SURROUND51, - HIGH_SURROUND51, - SURROUND71, - HIGH_SURROUND71, - MAX_STREAM_CONFIG -}; - -struct opus_stream_config_t { - std::int32_t sampleRate; - int channelCount; - int streams; - int coupledStreams; - const std::uint8_t *mapping; - int bitrate; -}; - -extern opus_stream_config_t stream_configs[MAX_STREAM_CONFIG]; - -struct config_t { - enum flags_e : int { - HIGH_QUALITY, - HOST_AUDIO, - MAX_FLAGS + enum stream_config_e : int { + STEREO, + HIGH_STEREO, + SURROUND51, + HIGH_SURROUND51, + SURROUND71, + HIGH_SURROUND71, + MAX_STREAM_CONFIG }; - int packetDuration; - int channels; - int mask; + struct opus_stream_config_t { + std::int32_t sampleRate; + int channelCount; + int streams; + int coupledStreams; + const std::uint8_t *mapping; + int bitrate; + }; - std::bitset flags; -}; + extern opus_stream_config_t stream_configs[MAX_STREAM_CONFIG]; -using buffer_t = util::buffer_t; -using packet_t = std::pair; -void capture(safe::mail_t mail, config_t config, void *channel_data); -} // namespace audio + struct config_t { + enum flags_e : int { + HIGH_QUALITY, + HOST_AUDIO, + MAX_FLAGS + }; + + int packetDuration; + int channels; + int mask; + + std::bitset flags; + }; + + using buffer_t = util::buffer_t; + using packet_t = std::pair; + void + capture(safe::mail_t mail, config_t config, void *channel_data); +} // namespace audio #endif diff --git a/src/cbs.cpp b/src/cbs.cpp index 6844d91a..eedaaff0 100644 --- a/src/cbs.cpp +++ b/src/cbs.cpp @@ -12,289 +12,293 @@ extern "C" { using namespace std::literals; namespace cbs { -void close(CodedBitstreamContext *c) { - ff_cbs_close(&c); -} - -using ctx_t = util::safe_ptr; - -class frag_t : public CodedBitstreamFragment { -public: - frag_t(frag_t &&o) { - std::copy((std::uint8_t *)&o, (std::uint8_t *)(&o + 1), (std::uint8_t *)this); - - o.data = nullptr; - o.units = nullptr; - }; - - frag_t() { - std::fill_n((std::uint8_t *)this, sizeof(*this), 0); + void + close(CodedBitstreamContext *c) { + ff_cbs_close(&c); } - frag_t &operator=(frag_t &&o) { - std::copy((std::uint8_t *)&o, (std::uint8_t *)(&o + 1), (std::uint8_t *)this); + using ctx_t = util::safe_ptr; - o.data = nullptr; - o.units = nullptr; + class frag_t: public CodedBitstreamFragment { + public: + frag_t(frag_t &&o) { + std::copy((std::uint8_t *) &o, (std::uint8_t *) (&o + 1), (std::uint8_t *) this); - return *this; - }; + o.data = nullptr; + o.units = nullptr; + }; - - ~frag_t() { - if(data || units) { - ff_cbs_fragment_free(this); + frag_t() { + std::fill_n((std::uint8_t *) this, sizeof(*this), 0); } - } -}; -util::buffer_t write(const cbs::ctx_t &cbs_ctx, std::uint8_t nal, void *uh, AVCodecID codec_id) { - cbs::frag_t frag; - auto err = ff_cbs_insert_unit_content(&frag, -1, nal, uh, nullptr); - if(err < 0) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) << "Could not insert NAL unit SPS: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + frag_t & + operator=(frag_t &&o) { + std::copy((std::uint8_t *) &o, (std::uint8_t *) (&o + 1), (std::uint8_t *) this); - return {}; - } + o.data = nullptr; + o.units = nullptr; - err = ff_cbs_write_fragment_data(cbs_ctx.get(), &frag); - if(err < 0) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) << "Could not write fragment data: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + return *this; + }; - return {}; - } + ~frag_t() { + if (data || units) { + ff_cbs_fragment_free(this); + } + } + }; - // frag.data_size * 8 - frag.data_bit_padding == bits in fragment - util::buffer_t data { frag.data_size }; - std::copy_n(frag.data, frag.data_size, std::begin(data)); - - return data; -} - -util::buffer_t write(std::uint8_t nal, void *uh, AVCodecID codec_id) { - cbs::ctx_t cbs_ctx; - ff_cbs_init(&cbs_ctx, codec_id, nullptr); - - return write(cbs_ctx, nal, uh, codec_id); -} - -util::buffer_t make_sps_h264(const AVCodecContext *ctx) { - H264RawSPS sps {}; - - /* b_per_p == ctx->max_b_frames for h264 */ - /* desired_b_depth == avoption("b_depth") == 1 */ - /* max_b_depth == std::min(av_log2(ctx->b_per_p) + 1, desired_b_depth) ==> 1 */ - auto max_b_depth = 1; - auto dpb_frame = ctx->gop_size == 1 ? 0 : 1 + max_b_depth; - auto mb_width = (FFALIGN(ctx->width, 16) / 16) * 16; - auto mb_height = (FFALIGN(ctx->height, 16) / 16) * 16; - - - sps.nal_unit_header.nal_ref_idc = 3; - sps.nal_unit_header.nal_unit_type = H264_NAL_SPS; - - sps.profile_idc = FF_PROFILE_H264_HIGH & 0xFF; - - sps.constraint_set1_flag = 1; - - if(ctx->level != FF_LEVEL_UNKNOWN) { - sps.level_idc = ctx->level; - } - else { - auto framerate = ctx->framerate; - - auto level = ff_h264_guess_level( - sps.profile_idc, - ctx->bit_rate, - framerate.num / framerate.den, - mb_width, - mb_height, - dpb_frame); - - if(!level) { - BOOST_LOG(error) << "Could not guess h264 level"sv; + util::buffer_t + write(const cbs::ctx_t &cbs_ctx, std::uint8_t nal, void *uh, AVCodecID codec_id) { + cbs::frag_t frag; + auto err = ff_cbs_insert_unit_content(&frag, -1, nal, uh, nullptr); + if (err < 0) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + BOOST_LOG(error) << "Could not insert NAL unit SPS: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); return {}; } - sps.level_idc = level->level_idc; + + err = ff_cbs_write_fragment_data(cbs_ctx.get(), &frag); + if (err < 0) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + BOOST_LOG(error) << "Could not write fragment data: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + + return {}; + } + + // frag.data_size * 8 - frag.data_bit_padding == bits in fragment + util::buffer_t data { frag.data_size }; + std::copy_n(frag.data, frag.data_size, std::begin(data)); + + return data; } - sps.seq_parameter_set_id = 0; - sps.chroma_format_idc = 1; + util::buffer_t + write(std::uint8_t nal, void *uh, AVCodecID codec_id) { + cbs::ctx_t cbs_ctx; + ff_cbs_init(&cbs_ctx, codec_id, nullptr); - sps.log2_max_frame_num_minus4 = 3; // 4; - sps.pic_order_cnt_type = 0; - sps.log2_max_pic_order_cnt_lsb_minus4 = 0; // 4; - - sps.max_num_ref_frames = dpb_frame; - - sps.pic_width_in_mbs_minus1 = mb_width / 16 - 1; - sps.pic_height_in_map_units_minus1 = mb_height / 16 - 1; - - sps.frame_mbs_only_flag = 1; - sps.direct_8x8_inference_flag = 1; - - if(ctx->width != mb_width || ctx->height != mb_height) { - sps.frame_cropping_flag = 1; - sps.frame_crop_left_offset = 0; - sps.frame_crop_top_offset = 0; - sps.frame_crop_right_offset = (mb_width - ctx->width) / 2; - sps.frame_crop_bottom_offset = (mb_height - ctx->height) / 2; + return write(cbs_ctx, nal, uh, codec_id); } - sps.vui_parameters_present_flag = 1; + util::buffer_t + make_sps_h264(const AVCodecContext *ctx) { + H264RawSPS sps {}; - auto &vui = sps.vui; + /* b_per_p == ctx->max_b_frames for h264 */ + /* desired_b_depth == avoption("b_depth") == 1 */ + /* max_b_depth == std::min(av_log2(ctx->b_per_p) + 1, desired_b_depth) ==> 1 */ + auto max_b_depth = 1; + auto dpb_frame = ctx->gop_size == 1 ? 0 : 1 + max_b_depth; + auto mb_width = (FFALIGN(ctx->width, 16) / 16) * 16; + auto mb_height = (FFALIGN(ctx->height, 16) / 16) * 16; - vui.video_format = 5; - vui.colour_description_present_flag = 1; - vui.video_signal_type_present_flag = 1; - vui.video_full_range_flag = ctx->color_range == AVCOL_RANGE_JPEG; - vui.colour_primaries = ctx->color_primaries; - vui.transfer_characteristics = ctx->color_trc; - vui.matrix_coefficients = ctx->colorspace; + sps.nal_unit_header.nal_ref_idc = 3; + sps.nal_unit_header.nal_unit_type = H264_NAL_SPS; - vui.low_delay_hrd_flag = 1 - vui.fixed_frame_rate_flag; + sps.profile_idc = FF_PROFILE_H264_HIGH & 0xFF; - vui.bitstream_restriction_flag = 1; - vui.motion_vectors_over_pic_boundaries_flag = 1; - vui.log2_max_mv_length_horizontal = 15; - vui.log2_max_mv_length_vertical = 15; - vui.max_num_reorder_frames = max_b_depth; - vui.max_dec_frame_buffering = max_b_depth + 1; + sps.constraint_set1_flag = 1; - return write(sps.nal_unit_header.nal_unit_type, (void *)&sps.nal_unit_header, AV_CODEC_ID_H264); -} + if (ctx->level != FF_LEVEL_UNKNOWN) { + sps.level_idc = ctx->level; + } + else { + auto framerate = ctx->framerate; -hevc_t make_sps_hevc(const AVCodecContext *avctx, const AVPacket *packet) { - cbs::ctx_t ctx; - if(ff_cbs_init(&ctx, AV_CODEC_ID_H265, nullptr)) { - return {}; + auto level = ff_h264_guess_level( + sps.profile_idc, + ctx->bit_rate, + framerate.num / framerate.den, + mb_width, + mb_height, + dpb_frame); + + if (!level) { + BOOST_LOG(error) << "Could not guess h264 level"sv; + + return {}; + } + sps.level_idc = level->level_idc; + } + + sps.seq_parameter_set_id = 0; + sps.chroma_format_idc = 1; + + sps.log2_max_frame_num_minus4 = 3; // 4; + sps.pic_order_cnt_type = 0; + sps.log2_max_pic_order_cnt_lsb_minus4 = 0; // 4; + + sps.max_num_ref_frames = dpb_frame; + + sps.pic_width_in_mbs_minus1 = mb_width / 16 - 1; + sps.pic_height_in_map_units_minus1 = mb_height / 16 - 1; + + sps.frame_mbs_only_flag = 1; + sps.direct_8x8_inference_flag = 1; + + if (ctx->width != mb_width || ctx->height != mb_height) { + sps.frame_cropping_flag = 1; + sps.frame_crop_left_offset = 0; + sps.frame_crop_top_offset = 0; + sps.frame_crop_right_offset = (mb_width - ctx->width) / 2; + sps.frame_crop_bottom_offset = (mb_height - ctx->height) / 2; + } + + sps.vui_parameters_present_flag = 1; + + auto &vui = sps.vui; + + vui.video_format = 5; + vui.colour_description_present_flag = 1; + vui.video_signal_type_present_flag = 1; + vui.video_full_range_flag = ctx->color_range == AVCOL_RANGE_JPEG; + vui.colour_primaries = ctx->color_primaries; + vui.transfer_characteristics = ctx->color_trc; + vui.matrix_coefficients = ctx->colorspace; + + vui.low_delay_hrd_flag = 1 - vui.fixed_frame_rate_flag; + + vui.bitstream_restriction_flag = 1; + vui.motion_vectors_over_pic_boundaries_flag = 1; + vui.log2_max_mv_length_horizontal = 15; + vui.log2_max_mv_length_vertical = 15; + vui.max_num_reorder_frames = max_b_depth; + vui.max_dec_frame_buffering = max_b_depth + 1; + + return write(sps.nal_unit_header.nal_unit_type, (void *) &sps.nal_unit_header, AV_CODEC_ID_H264); } - cbs::frag_t frag; + hevc_t + make_sps_hevc(const AVCodecContext *avctx, const AVPacket *packet) { + cbs::ctx_t ctx; + if (ff_cbs_init(&ctx, AV_CODEC_ID_H265, nullptr)) { + return {}; + } - int err = ff_cbs_read_packet(ctx.get(), &frag, packet); - if(err < 0) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + cbs::frag_t frag; - return {}; + int err = ff_cbs_read_packet(ctx.get(), &frag, packet); + if (err < 0) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + + return {}; + } + + auto vps_p = ((CodedBitstreamH265Context *) ctx->priv_data)->active_vps; + auto sps_p = ((CodedBitstreamH265Context *) ctx->priv_data)->active_sps; + + H265RawSPS sps { *sps_p }; + H265RawVPS vps { *vps_p }; + + vps.profile_tier_level.general_profile_compatibility_flag[4] = 1; + sps.profile_tier_level.general_profile_compatibility_flag[4] = 1; + + auto &vui = sps.vui; + std::memset(&vui, 0, sizeof(vui)); + + sps.vui_parameters_present_flag = 1; + + // skip sample aspect ratio + + vui.video_format = 5; + vui.colour_description_present_flag = 1; + vui.video_signal_type_present_flag = 1; + vui.video_full_range_flag = avctx->color_range == AVCOL_RANGE_JPEG; + vui.colour_primaries = avctx->color_primaries; + vui.transfer_characteristics = avctx->color_trc; + vui.matrix_coefficients = avctx->colorspace; + + vui.vui_timing_info_present_flag = vps.vps_timing_info_present_flag; + vui.vui_num_units_in_tick = vps.vps_num_units_in_tick; + vui.vui_time_scale = vps.vps_time_scale; + vui.vui_poc_proportional_to_timing_flag = vps.vps_poc_proportional_to_timing_flag; + vui.vui_num_ticks_poc_diff_one_minus1 = vps.vps_num_ticks_poc_diff_one_minus1; + vui.vui_hrd_parameters_present_flag = 0; + + vui.bitstream_restriction_flag = 1; + vui.motion_vectors_over_pic_boundaries_flag = 1; + vui.restricted_ref_pic_lists_flag = 1; + vui.max_bytes_per_pic_denom = 0; + vui.max_bits_per_min_cu_denom = 0; + vui.log2_max_mv_length_horizontal = 15; + vui.log2_max_mv_length_vertical = 15; + + cbs::ctx_t write_ctx; + ff_cbs_init(&write_ctx, AV_CODEC_ID_H265, nullptr); + + return hevc_t { + nal_t { + write(write_ctx, vps.nal_unit_header.nal_unit_type, (void *) &vps.nal_unit_header, AV_CODEC_ID_H265), + write(ctx, vps_p->nal_unit_header.nal_unit_type, (void *) &vps_p->nal_unit_header, AV_CODEC_ID_H265), + }, + + nal_t { + write(write_ctx, sps.nal_unit_header.nal_unit_type, (void *) &sps.nal_unit_header, AV_CODEC_ID_H265), + write(ctx, sps_p->nal_unit_header.nal_unit_type, (void *) &sps_p->nal_unit_header, AV_CODEC_ID_H265), + }, + }; } + util::buffer_t + read_sps_h264(const AVPacket *packet) { + cbs::ctx_t ctx; + if (ff_cbs_init(&ctx, AV_CODEC_ID_H264, nullptr)) { + return {}; + } - auto vps_p = ((CodedBitstreamH265Context *)ctx->priv_data)->active_vps; - auto sps_p = ((CodedBitstreamH265Context *)ctx->priv_data)->active_sps; + cbs::frag_t frag; - H265RawSPS sps { *sps_p }; - H265RawVPS vps { *vps_p }; + int err = ff_cbs_read_packet(ctx.get(), &frag, &*packet); + if (err < 0) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); - vps.profile_tier_level.general_profile_compatibility_flag[4] = 1; - sps.profile_tier_level.general_profile_compatibility_flag[4] = 1; + return {}; + } - auto &vui = sps.vui; - std::memset(&vui, 0, sizeof(vui)); - - sps.vui_parameters_present_flag = 1; - - // skip sample aspect ratio - - vui.video_format = 5; - vui.colour_description_present_flag = 1; - vui.video_signal_type_present_flag = 1; - vui.video_full_range_flag = avctx->color_range == AVCOL_RANGE_JPEG; - vui.colour_primaries = avctx->color_primaries; - vui.transfer_characteristics = avctx->color_trc; - vui.matrix_coefficients = avctx->colorspace; - - - vui.vui_timing_info_present_flag = vps.vps_timing_info_present_flag; - vui.vui_num_units_in_tick = vps.vps_num_units_in_tick; - vui.vui_time_scale = vps.vps_time_scale; - vui.vui_poc_proportional_to_timing_flag = vps.vps_poc_proportional_to_timing_flag; - vui.vui_num_ticks_poc_diff_one_minus1 = vps.vps_num_ticks_poc_diff_one_minus1; - vui.vui_hrd_parameters_present_flag = 0; - - vui.bitstream_restriction_flag = 1; - vui.motion_vectors_over_pic_boundaries_flag = 1; - vui.restricted_ref_pic_lists_flag = 1; - vui.max_bytes_per_pic_denom = 0; - vui.max_bits_per_min_cu_denom = 0; - vui.log2_max_mv_length_horizontal = 15; - vui.log2_max_mv_length_vertical = 15; - - cbs::ctx_t write_ctx; - ff_cbs_init(&write_ctx, AV_CODEC_ID_H265, nullptr); - - - return hevc_t { - nal_t { - write(write_ctx, vps.nal_unit_header.nal_unit_type, (void *)&vps.nal_unit_header, AV_CODEC_ID_H265), - write(ctx, vps_p->nal_unit_header.nal_unit_type, (void *)&vps_p->nal_unit_header, AV_CODEC_ID_H265), - }, - - nal_t { - write(write_ctx, sps.nal_unit_header.nal_unit_type, (void *)&sps.nal_unit_header, AV_CODEC_ID_H265), - write(ctx, sps_p->nal_unit_header.nal_unit_type, (void *)&sps_p->nal_unit_header, AV_CODEC_ID_H265), - }, - }; -} - -util::buffer_t read_sps_h264(const AVPacket *packet) { - cbs::ctx_t ctx; - if(ff_cbs_init(&ctx, AV_CODEC_ID_H264, nullptr)) { - return {}; + auto h264 = (H264RawNALUnitHeader *) ((CodedBitstreamH264Context *) ctx->priv_data)->active_sps; + return write(h264->nal_unit_type, (void *) h264, AV_CODEC_ID_H264); } - cbs::frag_t frag; - - int err = ff_cbs_read_packet(ctx.get(), &frag, &*packet); - if(err < 0) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); - - return {}; + h264_t + make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet) { + return h264_t { + make_sps_h264(ctx), + read_sps_h264(packet), + }; } - auto h264 = (H264RawNALUnitHeader *)((CodedBitstreamH264Context *)ctx->priv_data)->active_sps; - return write(h264->nal_unit_type, (void *)h264, AV_CODEC_ID_H264); -} - -h264_t make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet) { - return h264_t { - make_sps_h264(ctx), - read_sps_h264(packet), - }; -} - -bool validate_sps(const AVPacket *packet, int codec_id) { - cbs::ctx_t ctx; - if(ff_cbs_init(&ctx, (AVCodecID)codec_id, nullptr)) { - return false; - } - - cbs::frag_t frag; - - int err = ff_cbs_read_packet(ctx.get(), &frag, packet); - if(err < 0) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); - - return false; - } - - if(codec_id == AV_CODEC_ID_H264) { - auto h264 = (CodedBitstreamH264Context *)ctx->priv_data; - - if(!h264->active_sps->vui_parameters_present_flag) { + bool + validate_sps(const AVPacket *packet, int codec_id) { + cbs::ctx_t ctx; + if (ff_cbs_init(&ctx, (AVCodecID) codec_id, nullptr)) { return false; } - return true; - } + cbs::frag_t frag; - return ((CodedBitstreamH265Context *)ctx->priv_data)->active_sps->vui_parameters_present_flag; -} -} // namespace cbs + int err = ff_cbs_read_packet(ctx.get(), &frag, packet); + if (err < 0) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + BOOST_LOG(error) << "Couldn't read packet: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + + return false; + } + + if (codec_id == AV_CODEC_ID_H264) { + auto h264 = (CodedBitstreamH264Context *) ctx->priv_data; + + if (!h264->active_sps->vui_parameters_present_flag) { + return false; + } + + return true; + } + + return ((CodedBitstreamH265Context *) ctx->priv_data)->active_sps->vui_parameters_present_flag; + } +} // namespace cbs diff --git a/src/cbs.h b/src/cbs.h index cd989b4a..fe532b98 100644 --- a/src/cbs.h +++ b/src/cbs.h @@ -8,27 +8,30 @@ struct AVCodecContext; namespace cbs { -struct nal_t { - util::buffer_t _new; - util::buffer_t old; -}; + struct nal_t { + util::buffer_t _new; + util::buffer_t old; + }; -struct hevc_t { - nal_t vps; - nal_t sps; -}; + struct hevc_t { + nal_t vps; + nal_t sps; + }; -struct h264_t { - nal_t sps; -}; + struct h264_t { + nal_t sps; + }; -hevc_t make_sps_hevc(const AVCodecContext *ctx, const AVPacket *packet); -h264_t make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet); + hevc_t + make_sps_hevc(const AVCodecContext *ctx, const AVPacket *packet); + h264_t + make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet); -/** + /** * Check if SPS->VUI is present */ -bool validate_sps(const AVPacket *packet, int codec_id); -} // namespace cbs + bool + validate_sps(const AVPacket *packet, int codec_id); +} // namespace cbs #endif diff --git a/src/config.cpp b/src/config.cpp index 0aecc3ca..2f16cbff 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -26,1094 +26,1134 @@ using namespace std::literals; #define APPS_JSON_PATH platf::appdata().string() + "/apps.json" namespace config { -namespace nv { + namespace nv { #ifdef __APPLE__ -// values accurate as of 27/12/2022, but aren't strictly necessary for MacOS build -#define NV_ENC_TUNING_INFO_HIGH_QUALITY 1 -#define NV_ENC_TUNING_INFO_LOW_LATENCY 2 -#define NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY 3 -#define NV_ENC_TUNING_INFO_LOSSLESS 4 -#define NV_ENC_PARAMS_RC_CONSTQP 0x0 -#define NV_ENC_PARAMS_RC_VBR 0x1 -#define NV_ENC_PARAMS_RC_CBR 0x2 -#define NV_ENC_H264_ENTROPY_CODING_MODE_CABAC 1 -#define NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC 2 + // values accurate as of 27/12/2022, but aren't strictly necessary for MacOS build + #define NV_ENC_TUNING_INFO_HIGH_QUALITY 1 + #define NV_ENC_TUNING_INFO_LOW_LATENCY 2 + #define NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY 3 + #define NV_ENC_TUNING_INFO_LOSSLESS 4 + #define NV_ENC_PARAMS_RC_CONSTQP 0x0 + #define NV_ENC_PARAMS_RC_VBR 0x1 + #define NV_ENC_PARAMS_RC_CBR 0x2 + #define NV_ENC_H264_ENTROPY_CODING_MODE_CABAC 1 + #define NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC 2 #else -#include + #include #endif -enum preset_e : int { - p1 = 12, // PRESET_P1, // must be kept in sync with - p2, // PRESET_P2, - p3, // PRESET_P3, - p4, // PRESET_P4, - p5, // PRESET_P5, - p6, // PRESET_P6, - p7 // PRESET_P7 -}; + enum preset_e : int { + p1 = 12, // PRESET_P1, // must be kept in sync with + p2, // PRESET_P2, + p3, // PRESET_P3, + p4, // PRESET_P4, + p5, // PRESET_P5, + p6, // PRESET_P6, + p7 // PRESET_P7 + }; -enum tune_e : int { - hq = NV_ENC_TUNING_INFO_HIGH_QUALITY, - ll = NV_ENC_TUNING_INFO_LOW_LATENCY, - ull = NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY, - lossless = NV_ENC_TUNING_INFO_LOSSLESS -}; + enum tune_e : int { + hq = NV_ENC_TUNING_INFO_HIGH_QUALITY, + ll = NV_ENC_TUNING_INFO_LOW_LATENCY, + ull = NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY, + lossless = NV_ENC_TUNING_INFO_LOSSLESS + }; -enum rc_e : int { - constqp = NV_ENC_PARAMS_RC_CONSTQP, /**< Constant QP mode */ - vbr = NV_ENC_PARAMS_RC_VBR, /**< Variable bitrate mode */ - cbr = NV_ENC_PARAMS_RC_CBR /**< Constant bitrate mode */ -}; + enum rc_e : int { + constqp = NV_ENC_PARAMS_RC_CONSTQP, /**< Constant QP mode */ + vbr = NV_ENC_PARAMS_RC_VBR, /**< Variable bitrate mode */ + cbr = NV_ENC_PARAMS_RC_CBR /**< Constant bitrate mode */ + }; -enum coder_e : int { - _auto = 0, - cabac = NV_ENC_H264_ENTROPY_CODING_MODE_CABAC, - cavlc = NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC, -}; + enum coder_e : int { + _auto = 0, + cabac = NV_ENC_H264_ENTROPY_CODING_MODE_CABAC, + cavlc = NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC, + }; -std::optional preset_from_view(const std::string_view &preset) { + std::optional + preset_from_view(const std::string_view &preset) { #define _CONVERT_(x) \ - if(preset == #x##sv) return x - _CONVERT_(p1); - _CONVERT_(p2); - _CONVERT_(p3); - _CONVERT_(p4); - _CONVERT_(p5); - _CONVERT_(p6); - _CONVERT_(p7); + if (preset == #x##sv) return x + _CONVERT_(p1); + _CONVERT_(p2); + _CONVERT_(p3); + _CONVERT_(p4); + _CONVERT_(p5); + _CONVERT_(p6); + _CONVERT_(p7); #undef _CONVERT_ - return std::nullopt; -} + return std::nullopt; + } -std::optional tune_from_view(const std::string_view &tune) { + std::optional + tune_from_view(const std::string_view &tune) { #define _CONVERT_(x) \ - if(tune == #x##sv) return x - _CONVERT_(hq); - _CONVERT_(ll); - _CONVERT_(ull); - _CONVERT_(lossless); + if (tune == #x##sv) return x + _CONVERT_(hq); + _CONVERT_(ll); + _CONVERT_(ull); + _CONVERT_(lossless); #undef _CONVERT_ - return std::nullopt; -} + return std::nullopt; + } -std::optional rc_from_view(const std::string_view &rc) { + std::optional + rc_from_view(const std::string_view &rc) { #define _CONVERT_(x) \ - if(rc == #x##sv) return x - _CONVERT_(constqp); - _CONVERT_(vbr); - _CONVERT_(cbr); + if (rc == #x##sv) return x + _CONVERT_(constqp); + _CONVERT_(vbr); + _CONVERT_(cbr); #undef _CONVERT_ - return std::nullopt; -} + return std::nullopt; + } -int coder_from_view(const std::string_view &coder) { - if(coder == "auto"sv) return _auto; - if(coder == "cabac"sv || coder == "ac"sv) return cabac; - if(coder == "cavlc"sv || coder == "vlc"sv) return cavlc; + int + coder_from_view(const std::string_view &coder) { + if (coder == "auto"sv) return _auto; + if (coder == "cabac"sv || coder == "ac"sv) return cabac; + if (coder == "cavlc"sv || coder == "vlc"sv) return cavlc; - return -1; -} -} // namespace nv + return -1; + } + } // namespace nv -namespace amd { + namespace amd { #ifdef __APPLE__ -// values accurate as of 27/12/2022, but aren't strictly necessary for MacOS build -#define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED 10 -#define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY 0 -#define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_BALANCED 5 -#define AMF_VIDEO_ENCODER_QUALITY_PRESET_SPEED 1 -#define AMF_VIDEO_ENCODER_QUALITY_PRESET_QUALITY 2 -#define AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED 0 -#define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP 0 -#define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR 3 -#define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2 -#define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 1 -#define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP 0 -#define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR 1 -#define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2 -#define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 3 -#define AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCONDING 0 -#define AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY 1 -#define AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY 2 -#define AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM 3 -#define AMF_VIDEO_ENCODER_USAGE_TRANSCONDING 0 -#define AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY 1 -#define AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY 2 -#define AMF_VIDEO_ENCODER_USAGE_WEBCAM 3 -#define AMF_VIDEO_ENCODER_UNDEFINED 0 -#define AMF_VIDEO_ENCODER_CABAC 1 -#define AMF_VIDEO_ENCODER_CALV 2 + // values accurate as of 27/12/2022, but aren't strictly necessary for MacOS build + #define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED 10 + #define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY 0 + #define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_BALANCED 5 + #define AMF_VIDEO_ENCODER_QUALITY_PRESET_SPEED 1 + #define AMF_VIDEO_ENCODER_QUALITY_PRESET_QUALITY 2 + #define AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED 0 + #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP 0 + #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR 3 + #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2 + #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 1 + #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP 0 + #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR 1 + #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2 + #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 3 + #define AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCONDING 0 + #define AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY 1 + #define AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY 2 + #define AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM 3 + #define AMF_VIDEO_ENCODER_USAGE_TRANSCONDING 0 + #define AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY 1 + #define AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY 2 + #define AMF_VIDEO_ENCODER_USAGE_WEBCAM 3 + #define AMF_VIDEO_ENCODER_UNDEFINED 0 + #define AMF_VIDEO_ENCODER_CABAC 1 + #define AMF_VIDEO_ENCODER_CALV 2 #else -#include -#include + #include + #include #endif -enum class quality_hevc_e : int { - speed = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED, - quality = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY, - balanced = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_BALANCED -}; + enum class quality_hevc_e : int { + speed = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED, + quality = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY, + balanced = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_BALANCED + }; -enum class quality_h264_e : int { - speed = AMF_VIDEO_ENCODER_QUALITY_PRESET_SPEED, - quality = AMF_VIDEO_ENCODER_QUALITY_PRESET_QUALITY, - balanced = AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED -}; + enum class quality_h264_e : int { + speed = AMF_VIDEO_ENCODER_QUALITY_PRESET_SPEED, + quality = AMF_VIDEO_ENCODER_QUALITY_PRESET_QUALITY, + balanced = AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED + }; -enum class rc_hevc_e : int { - cqp = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP, - vbr_latency = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR, - vbr_peak = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR, - cbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR -}; + enum class rc_hevc_e : int { + cqp = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP, + vbr_latency = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR, + vbr_peak = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR, + cbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR + }; -enum class rc_h264_e : int { - cqp = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP, - vbr_latency = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR, - vbr_peak = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR, - cbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR -}; + enum class rc_h264_e : int { + cqp = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP, + vbr_latency = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR, + vbr_peak = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR, + cbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR + }; -enum class usage_hevc_e : int { - transcoding = AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCONDING, - webcam = AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM, - lowlatency = AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY, - ultralowlatency = AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY -}; + enum class usage_hevc_e : int { + transcoding = AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCONDING, + webcam = AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM, + lowlatency = AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY, + ultralowlatency = AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY + }; -enum class usage_h264_e : int { - transcoding = AMF_VIDEO_ENCODER_USAGE_TRANSCONDING, - webcam = AMF_VIDEO_ENCODER_USAGE_WEBCAM, - lowlatency = AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY, - ultralowlatency = AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY -}; + enum class usage_h264_e : int { + transcoding = AMF_VIDEO_ENCODER_USAGE_TRANSCONDING, + webcam = AMF_VIDEO_ENCODER_USAGE_WEBCAM, + lowlatency = AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY, + ultralowlatency = AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY + }; -enum coder_e : int { - _auto = AMF_VIDEO_ENCODER_UNDEFINED, - cabac = AMF_VIDEO_ENCODER_CABAC, - cavlc = AMF_VIDEO_ENCODER_CALV -}; + enum coder_e : int { + _auto = AMF_VIDEO_ENCODER_UNDEFINED, + cabac = AMF_VIDEO_ENCODER_CABAC, + cavlc = AMF_VIDEO_ENCODER_CALV + }; -std::optional quality_from_view(const std::string_view &quality_type, int codec) { + std::optional + quality_from_view(const std::string_view &quality_type, int codec) { #define _CONVERT_(x) \ - if(quality_type == #x##sv) return codec == 0 ? (int)quality_hevc_e::x : (int)quality_h264_e::x - _CONVERT_(quality); - _CONVERT_(speed); - _CONVERT_(balanced); + if (quality_type == #x##sv) return codec == 0 ? (int) quality_hevc_e::x : (int) quality_h264_e::x + _CONVERT_(quality); + _CONVERT_(speed); + _CONVERT_(balanced); #undef _CONVERT_ - return std::nullopt; -} - -std::optional rc_from_view(const std::string_view &rc, int codec) { -#define _CONVERT_(x) \ - if(rc == #x##sv) return codec == 0 ? (int)rc_hevc_e::x : (int)rc_h264_e::x - _CONVERT_(cqp); - _CONVERT_(vbr_latency); - _CONVERT_(vbr_peak); - _CONVERT_(cbr); -#undef _CONVERT_ - return std::nullopt; -} - -std::optional usage_from_view(const std::string_view &rc, int codec) { -#define _CONVERT_(x) \ - if(rc == #x##sv) return codec == 0 ? (int)usage_hevc_e::x : (int)usage_h264_e::x - _CONVERT_(transcoding); - _CONVERT_(webcam); - _CONVERT_(lowlatency); - _CONVERT_(ultralowlatency); -#undef _CONVERT_ - return std::nullopt; -} - -int coder_from_view(const std::string_view &coder) { - if(coder == "auto"sv) return _auto; - if(coder == "cabac"sv || coder == "ac"sv) return cabac; - if(coder == "cavlc"sv || coder == "vlc"sv) return cavlc; - - return -1; -} -} // namespace amd - -namespace qsv { -enum preset_e : int { - veryslow = 1, - slower = 2, - slow = 3, - medium = 4, - fast = 5, - faster = 6, - veryfast = 7 -}; - -enum cavlc_e : int { - _auto = false, - enabled = true, - disabled = false -}; - -std::optional preset_from_view(const std::string_view &preset) { -#define _CONVERT_(x) \ - if(preset == #x##sv) return x - _CONVERT_(veryslow); - _CONVERT_(slower); - _CONVERT_(slow); - _CONVERT_(medium); - _CONVERT_(fast); - _CONVERT_(faster); - _CONVERT_(veryfast); -#undef _CONVERT_ - return std::nullopt; -} - -std::optional coder_from_view(const std::string_view &coder) { - if(coder == "auto"sv) return _auto; - if(coder == "cabac"sv || coder == "ac"sv) return disabled; - if(coder == "cavlc"sv || coder == "vlc"sv) return enabled; - return std::nullopt; -} - -} // namespace qsv - -namespace vt { - -enum coder_e : int { - _auto = 0, - cabac, - cavlc -}; - -int coder_from_view(const std::string_view &coder) { - if(coder == "auto"sv) return _auto; - if(coder == "cabac"sv || coder == "ac"sv) return cabac; - if(coder == "cavlc"sv || coder == "vlc"sv) return cavlc; - - return -1; -} - -int allow_software_from_view(const std::string_view &software) { - if(software == "allowed"sv || software == "forced") return 1; - - return 0; -} - -int force_software_from_view(const std::string_view &software) { - if(software == "forced") return 1; - - return 0; -} - -int rt_from_view(const std::string_view &rt) { - if(rt == "disabled" || rt == "off" || rt == "0") return 0; - - return 1; -} - -} // namespace vt - -video_t video { - 28, // qp - - 0, // hevc_mode - - 1, // min_threads - { - "superfast"s, // preset - "zerolatency"s, // tune - }, // software - - { - nv::p4, // preset - nv::ull, // tune - nv::cbr, // rc - nv::_auto // coder - }, // nv - - { - qsv::medium, // preset - qsv::_auto, // cavlc - }, // qsv - - { - (int)amd::quality_h264_e::balanced, // quality (h264) - (int)amd::quality_hevc_e::balanced, // quality (hevc) - (int)amd::rc_h264_e::vbr_latency, // rate control (h264) - (int)amd::rc_hevc_e::vbr_latency, // rate control (hevc) - (int)amd::usage_h264_e::ultralowlatency, // usage (h264) - (int)amd::usage_hevc_e::ultralowlatency, // usage (hevc) - 0, // preanalysis - 1, // vbaq - (int)amd::coder_e::_auto, // coder - }, // amd - - { - 0, - 0, - 1, - -1, - }, // vt - - {}, // capture - {}, // encoder - {}, // adapter_name - {}, // output_name - true // dwmflush -}; - -audio_t audio {}; - -stream_t stream { - 10s, // ping_timeout - - APPS_JSON_PATH, - - 20, // fecPercentage - 1 // channels -}; - -nvhttp_t nvhttp { - "pc", // origin_pin - "lan", // origin web manager - - PRIVATE_KEY_FILE, - CERTIFICATE_FILE, - - boost::asio::ip::host_name(), // sunshine_name, - "sunshine_state.json"s, // file_state - {}, // external_ip - { - "352x240"s, - "480x360"s, - "858x480"s, - "1280x720"s, - "1920x1080"s, - "2560x1080"s, - "3440x1440"s - "1920x1200"s, - "3860x2160"s, - "3840x1600"s, - }, // supported resolutions - - { 10, 30, 60, 90, 120 }, // supported fps -}; - -input_t input { - { - { 0x10, 0xA0 }, - { 0x11, 0xA2 }, - { 0x12, 0xA4 }, - }, - 2s, // back_button_timeout - 500ms, // key_repeat_delay - std::chrono::duration { 1 / 24.9 }, // key_repeat_period - - { - platf::supported_gamepads().front().data(), - platf::supported_gamepads().front().size(), - }, // Default gamepad - - true, // keyboard enabled - true, // mouse enabled - true, // controller enabled -}; - -sunshine_t sunshine { - 2, // min_log_level - 0, // flags - {}, // User file - {}, // Username - {}, // Password - {}, // Password Salt - platf::appdata().string() + "/sunshine.conf", // config file - {}, // cmd args - 47989, - platf::appdata().string() + "/sunshine.log", // log file - {}, // prep commands -}; - -bool endline(char ch) { - return ch == '\r' || ch == '\n'; -} - -bool space_tab(char ch) { - return ch == ' ' || ch == '\t'; -} - -bool whitespace(char ch) { - return space_tab(ch) || endline(ch); -} - -std::string to_string(const char *begin, const char *end) { - std::string result; - - KITTY_WHILE_LOOP(auto pos = begin, pos != end, { - auto comment = std::find(pos, end, '#'); - auto endl = std::find_if(comment, end, endline); - - result.append(pos, comment); - - pos = endl; - }) - - return result; -} - -template -It skip_list(It skipper, It end) { - int stack = 1; - while(skipper != end && stack) { - if(*skipper == '[') { - ++stack; - } - if(*skipper == ']') { - --stack; + return std::nullopt; } - ++skipper; + std::optional + rc_from_view(const std::string_view &rc, int codec) { +#define _CONVERT_(x) \ + if (rc == #x##sv) return codec == 0 ? (int) rc_hevc_e::x : (int) rc_h264_e::x + _CONVERT_(cqp); + _CONVERT_(vbr_latency); + _CONVERT_(vbr_peak); + _CONVERT_(cbr); +#undef _CONVERT_ + return std::nullopt; + } + + std::optional + usage_from_view(const std::string_view &rc, int codec) { +#define _CONVERT_(x) \ + if (rc == #x##sv) return codec == 0 ? (int) usage_hevc_e::x : (int) usage_h264_e::x + _CONVERT_(transcoding); + _CONVERT_(webcam); + _CONVERT_(lowlatency); + _CONVERT_(ultralowlatency); +#undef _CONVERT_ + return std::nullopt; + } + + int + coder_from_view(const std::string_view &coder) { + if (coder == "auto"sv) return _auto; + if (coder == "cabac"sv || coder == "ac"sv) return cabac; + if (coder == "cavlc"sv || coder == "vlc"sv) return cavlc; + + return -1; + } + } // namespace amd + + namespace qsv { + enum preset_e : int { + veryslow = 1, + slower = 2, + slow = 3, + medium = 4, + fast = 5, + faster = 6, + veryfast = 7 + }; + + enum cavlc_e : int { + _auto = false, + enabled = true, + disabled = false + }; + + std::optional + preset_from_view(const std::string_view &preset) { +#define _CONVERT_(x) \ + if (preset == #x##sv) return x + _CONVERT_(veryslow); + _CONVERT_(slower); + _CONVERT_(slow); + _CONVERT_(medium); + _CONVERT_(fast); + _CONVERT_(faster); + _CONVERT_(veryfast); +#undef _CONVERT_ + return std::nullopt; + } + + std::optional + coder_from_view(const std::string_view &coder) { + if (coder == "auto"sv) return _auto; + if (coder == "cabac"sv || coder == "ac"sv) return disabled; + if (coder == "cavlc"sv || coder == "vlc"sv) return enabled; + return std::nullopt; + } + + } // namespace qsv + + namespace vt { + + enum coder_e : int { + _auto = 0, + cabac, + cavlc + }; + + int + coder_from_view(const std::string_view &coder) { + if (coder == "auto"sv) return _auto; + if (coder == "cabac"sv || coder == "ac"sv) return cabac; + if (coder == "cavlc"sv || coder == "vlc"sv) return cavlc; + + return -1; + } + + int + allow_software_from_view(const std::string_view &software) { + if (software == "allowed"sv || software == "forced") return 1; + + return 0; + } + + int + force_software_from_view(const std::string_view &software) { + if (software == "forced") return 1; + + return 0; + } + + int + rt_from_view(const std::string_view &rt) { + if (rt == "disabled" || rt == "off" || rt == "0") return 0; + + return 1; + } + + } // namespace vt + + video_t video { + 28, // qp + + 0, // hevc_mode + + 1, // min_threads + { + "superfast"s, // preset + "zerolatency"s, // tune + }, // software + + { + nv::p4, // preset + nv::ull, // tune + nv::cbr, // rc + nv::_auto // coder + }, // nv + + { + qsv::medium, // preset + qsv::_auto, // cavlc + }, // qsv + + { + (int) amd::quality_h264_e::balanced, // quality (h264) + (int) amd::quality_hevc_e::balanced, // quality (hevc) + (int) amd::rc_h264_e::vbr_latency, // rate control (h264) + (int) amd::rc_hevc_e::vbr_latency, // rate control (hevc) + (int) amd::usage_h264_e::ultralowlatency, // usage (h264) + (int) amd::usage_hevc_e::ultralowlatency, // usage (hevc) + 0, // preanalysis + 1, // vbaq + (int) amd::coder_e::_auto, // coder + }, // amd + + { + 0, + 0, + 1, + -1, + }, // vt + + {}, // capture + {}, // encoder + {}, // adapter_name + {}, // output_name + true // dwmflush + }; + + audio_t audio {}; + + stream_t stream { + 10s, // ping_timeout + + APPS_JSON_PATH, + + 20, // fecPercentage + 1 // channels + }; + + nvhttp_t nvhttp { + "pc", // origin_pin + "lan", // origin web manager + + PRIVATE_KEY_FILE, + CERTIFICATE_FILE, + + boost::asio::ip::host_name(), // sunshine_name, + "sunshine_state.json"s, // file_state + {}, // external_ip + { + "352x240"s, + "480x360"s, + "858x480"s, + "1280x720"s, + "1920x1080"s, + "2560x1080"s, + "3440x1440"s + "1920x1200"s, + "3860x2160"s, + "3840x1600"s, + }, // supported resolutions + + { 10, 30, 60, 90, 120 }, // supported fps + }; + + input_t input { + { + { 0x10, 0xA0 }, + { 0x11, 0xA2 }, + { 0x12, 0xA4 }, + }, + 2s, // back_button_timeout + 500ms, // key_repeat_delay + std::chrono::duration { 1 / 24.9 }, // key_repeat_period + + { + platf::supported_gamepads().front().data(), + platf::supported_gamepads().front().size(), + }, // Default gamepad + + true, // keyboard enabled + true, // mouse enabled + true, // controller enabled + }; + + sunshine_t sunshine { + 2, // min_log_level + 0, // flags + {}, // User file + {}, // Username + {}, // Password + {}, // Password Salt + platf::appdata().string() + "/sunshine.conf", // config file + {}, // cmd args + 47989, + platf::appdata().string() + "/sunshine.log", // log file + {}, // prep commands + }; + + bool + endline(char ch) { + return ch == '\r' || ch == '\n'; } - return skipper; -} - -std::pair< - std::string_view::const_iterator, - std::optional>> -parse_option(std::string_view::const_iterator begin, std::string_view::const_iterator end) { - begin = std::find_if_not(begin, end, whitespace); - auto endl = std::find_if(begin, end, endline); - auto endc = std::find(begin, endl, '#'); - endc = std::find_if(std::make_reverse_iterator(endc), std::make_reverse_iterator(begin), std::not_fn(whitespace)).base(); - - auto eq = std::find(begin, endc, '='); - if(eq == endc || eq == begin) { - return std::make_pair(endl, std::nullopt); + bool + space_tab(char ch) { + return ch == ' ' || ch == '\t'; } - auto end_name = std::find_if_not(std::make_reverse_iterator(eq), std::make_reverse_iterator(begin), space_tab).base(); - auto begin_val = std::find_if_not(eq + 1, endc, space_tab); - - if(begin_val == endl) { - return std::make_pair(endl, std::nullopt); + bool + whitespace(char ch) { + return space_tab(ch) || endline(ch); } - // Lists might contain newlines - if(*begin_val == '[') { - endl = skip_list(begin_val + 1, end); - if(endl == end) { - std::cout << "Warning: Config option ["sv << to_string(begin, end_name) << "] Missing ']'"sv; + std::string + to_string(const char *begin, const char *end) { + std::string result; + KITTY_WHILE_LOOP(auto pos = begin, pos != end, { + auto comment = std::find(pos, end, '#'); + auto endl = std::find_if(comment, end, endline); + + result.append(pos, comment); + + pos = endl; + }) + + return result; + } + + template + It + skip_list(It skipper, It end) { + int stack = 1; + while (skipper != end && stack) { + if (*skipper == '[') { + ++stack; + } + if (*skipper == ']') { + --stack; + } + + ++skipper; + } + + return skipper; + } + + std::pair< + std::string_view::const_iterator, + std::optional>> + parse_option(std::string_view::const_iterator begin, std::string_view::const_iterator end) { + begin = std::find_if_not(begin, end, whitespace); + auto endl = std::find_if(begin, end, endline); + auto endc = std::find(begin, endl, '#'); + endc = std::find_if(std::make_reverse_iterator(endc), std::make_reverse_iterator(begin), std::not_fn(whitespace)).base(); + + auto eq = std::find(begin, endc, '='); + if (eq == endc || eq == begin) { return std::make_pair(endl, std::nullopt); } - } - return std::make_pair( - endl, - std::make_pair(to_string(begin, end_name), to_string(begin_val, endl))); -} + auto end_name = std::find_if_not(std::make_reverse_iterator(eq), std::make_reverse_iterator(begin), space_tab).base(); + auto begin_val = std::find_if_not(eq + 1, endc, space_tab); -std::unordered_map parse_config(const std::string_view &file_content) { - std::unordered_map vars; - - auto pos = std::begin(file_content); - auto end = std::end(file_content); - - while(pos < end) { - // auto newline = std::find_if(pos, end, [](auto ch) { return ch == '\n' || ch == '\r'; }); - TUPLE_2D(endl, var, parse_option(pos, end)); - - pos = endl; - if(pos != end) { - pos += (*pos == '\r') ? 2 : 1; + if (begin_val == endl) { + return std::make_pair(endl, std::nullopt); } - if(!var) { - continue; - } + // Lists might contain newlines + if (*begin_val == '[') { + endl = skip_list(begin_val + 1, end); + if (endl == end) { + std::cout << "Warning: Config option ["sv << to_string(begin, end_name) << "] Missing ']'"sv; - vars.emplace(std::move(*var)); - } - - return vars; -} - -void string_f(std::unordered_map &vars, const std::string &name, std::string &input) { - auto it = vars.find(name); - if(it == std::end(vars)) { - return; - } - - input = std::move(it->second); - - vars.erase(it); -} - -void string_restricted_f(std::unordered_map &vars, const std::string &name, std::string &input, const std::vector &allowed_vals) { - std::string temp; - string_f(vars, name, temp); - - for(auto &allowed_val : allowed_vals) { - if(temp == allowed_val) { - input = std::move(temp); - return; - } - } -} - -void path_f(std::unordered_map &vars, const std::string &name, fs::path &input) { - // appdata needs to be retrieved once only - static auto appdata = platf::appdata(); - - std::string temp; - string_f(vars, name, temp); - - if(!temp.empty()) { - input = temp; - } - - if(input.is_relative()) { - input = appdata / input; - } - - auto dir = input; - dir.remove_filename(); - - // Ensure the directories exists - if(!fs::exists(dir)) { - fs::create_directories(dir); - } -} - -void path_f(std::unordered_map &vars, const std::string &name, std::string &input) { - fs::path temp = input; - - path_f(vars, name, temp); - - input = temp.string(); -} - -void int_f(std::unordered_map &vars, const std::string &name, int &input) { - auto it = vars.find(name); - - if(it == std::end(vars)) { - return; - } - - std::string_view val = it->second; - - // If value is something like: "756" instead of 756 - if(val.size() >= 2 && val[0] == '"') { - val = val.substr(1, val.size() - 2); - } - - // If that integer is in hexadecimal - if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) { - input = util::from_hex(val.substr(2)); - } - else { - input = util::from_view(val); - } - - vars.erase(it); -} - -void int_f(std::unordered_map &vars, const std::string &name, std::optional &input) { - auto it = vars.find(name); - - if(it == std::end(vars)) { - return; - } - - std::string_view val = it->second; - - // If value is something like: "756" instead of 756 - if(val.size() >= 2 && val[0] == '"') { - val = val.substr(1, val.size() - 2); - } - - // If that integer is in hexadecimal - if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) { - input = util::from_hex(val.substr(2)); - } - else { - input = util::from_view(val); - } - - vars.erase(it); -} - -template -void int_f(std::unordered_map &vars, const std::string &name, int &input, F &&f) { - std::string tmp; - string_f(vars, name, tmp); - if(!tmp.empty()) { - input = f(tmp); - } -} - -template -void int_f(std::unordered_map &vars, const std::string &name, std::optional &input, F &&f) { - std::string tmp; - string_f(vars, name, tmp); - if(!tmp.empty()) { - input = f(tmp); - } -} - -void int_between_f(std::unordered_map &vars, const std::string &name, int &input, const std::pair &range) { - int temp = input; - - int_f(vars, name, temp); - - TUPLE_2D_REF(lower, upper, range); - if(temp >= lower && temp <= upper) { - input = temp; - } -} - -bool to_bool(std::string &boolean) { - std::for_each(std::begin(boolean), std::end(boolean), [](char ch) { return (char)std::tolower(ch); }); - - return boolean == "true"sv || - boolean == "yes"sv || - boolean == "enable"sv || - boolean == "enabled"sv || - boolean == "on"sv || - (std::find(std::begin(boolean), std::end(boolean), '1') != std::end(boolean)); -} - -void bool_f(std::unordered_map &vars, const std::string &name, bool &input) { - std::string tmp; - string_f(vars, name, tmp); - - if(tmp.empty()) { - return; - } - - input = to_bool(tmp); -} - -void double_f(std::unordered_map &vars, const std::string &name, double &input) { - std::string tmp; - string_f(vars, name, tmp); - - if(tmp.empty()) { - return; - } - - char *c_str_p; - auto val = std::strtod(tmp.c_str(), &c_str_p); - - if(c_str_p == tmp.c_str()) { - return; - } - - input = val; -} - -void double_between_f(std::unordered_map &vars, const std::string &name, double &input, const std::pair &range) { - double temp = input; - - double_f(vars, name, temp); - - TUPLE_2D_REF(lower, upper, range); - if(temp >= lower && temp <= upper) { - input = temp; - } -} - -void list_string_f(std::unordered_map &vars, const std::string &name, std::vector &input) { - std::string string; - string_f(vars, name, string); - - if(string.empty()) { - return; - } - - input.clear(); - - auto begin = std::cbegin(string); - if(*begin == '[') { - ++begin; - } - - begin = std::find_if_not(begin, std::cend(string), whitespace); - if(begin == std::cend(string)) { - return; - } - - auto pos = begin; - while(pos < std::cend(string)) { - if(*pos == '[') { - pos = skip_list(pos + 1, std::cend(string)) + 1; - } - else if(*pos == ']') { - break; - } - else if(*pos == ',') { - input.emplace_back(begin, pos); - pos = begin = std::find_if_not(pos + 1, std::cend(string), whitespace); - } - else { - ++pos; - } - } - - if(pos != begin) { - input.emplace_back(begin, pos); - } -} - -void list_prep_cmd_f(std::unordered_map &vars, const std::string &name, std::vector &input) { - std::string string; - string_f(vars, name, string); - - std::stringstream jsonStream; - - // check if string is empty, i.e. when the value doesn't exist in the config file - if(string.empty()) { - return; - } - - // We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it. - jsonStream << "{\"prep_cmd\":" << string << "}"; - - boost::property_tree::ptree jsonTree; - boost::property_tree::read_json(jsonStream, jsonTree); - - for(auto &[_, prep_cmd] : jsonTree.get_child("prep_cmd"s)) { - auto do_cmd = prep_cmd.get("do"s); - auto undo_cmd = prep_cmd.get("undo"s); - - input.emplace_back( - std::move(do_cmd), - std::move(undo_cmd)); - } -} - -void list_int_f(std::unordered_map &vars, const std::string &name, std::vector &input) { - std::vector list; - list_string_f(vars, name, list); - - for(auto &el : list) { - std::string_view val = el; - - // If value is something like: "756" instead of 756 - if(val.size() >= 2 && val[0] == '"') { - val = val.substr(1, val.size() - 2); - } - - int tmp; - - // If the integer is a hexadecimal - if(val.size() >= 2 && val.substr(0, 2) == "0x"sv) { - tmp = util::from_hex(val.substr(2)); - } - else { - tmp = util::from_view(val); - } - input.emplace_back(tmp); - } -} - -void map_int_int_f(std::unordered_map &vars, const std::string &name, std::unordered_map &input) { - std::vector list; - list_int_f(vars, name, list); - - // The list needs to be a multiple of 2 - if(list.size() % 2) { - std::cout << "Warning: expected "sv << name << " to have a multiple of two elements --> not "sv << list.size() << std::endl; - return; - } - - int x = 0; - while(x < list.size()) { - auto key = list[x++]; - auto val = list[x++]; - - input.emplace(key, val); - } -} - -int apply_flags(const char *line) { - int ret = 0; - while(*line != '\0') { - switch(*line) { - case '0': - config::sunshine.flags[config::flag::PIN_STDIN].flip(); - break; - case '1': - config::sunshine.flags[config::flag::FRESH_STATE].flip(); - break; - case '2': - config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE].flip(); - break; - case 'p': - config::sunshine.flags[config::flag::UPNP].flip(); - break; - default: - std::cout << "Warning: Unrecognized flag: ["sv << *line << ']' << std::endl; - ret = -1; - } - - ++line; - } - - return ret; -} - -void apply_config(std::unordered_map &&vars) { - if(!fs::exists(stream.file_apps.c_str())) { - fs::copy_file(SUNSHINE_ASSETS_DIR "/apps.json", stream.file_apps); - } - - for(auto &[name, val] : vars) { - std::cout << "["sv << name << "] -- ["sv << val << ']' << std::endl; - } - - int_f(vars, "qp", video.qp); - int_f(vars, "min_threads", video.min_threads); - int_between_f(vars, "hevc_mode", video.hevc_mode, { 0, 3 }); - string_f(vars, "sw_preset", video.sw.sw_preset); - string_f(vars, "sw_tune", video.sw.sw_tune); - int_f(vars, "nv_preset", video.nv.nv_preset, nv::preset_from_view); - int_f(vars, "nv_tune", video.nv.nv_tune, nv::tune_from_view); - int_f(vars, "nv_rc", video.nv.nv_rc, nv::rc_from_view); - int_f(vars, "nv_coder", video.nv.nv_coder, nv::coder_from_view); - - int_f(vars, "qsv_preset", video.qsv.qsv_preset, qsv::preset_from_view); - int_f(vars, "qsv_coder", video.qsv.qsv_cavlc, qsv::coder_from_view); - - std::string quality; - string_f(vars, "amd_quality", quality); - if(!quality.empty()) { - video.amd.amd_quality_h264 = amd::quality_from_view(quality, 1); - video.amd.amd_quality_hevc = amd::quality_from_view(quality, 0); - } - - std::string rc; - string_f(vars, "amd_rc", rc); - int_f(vars, "amd_coder", video.amd.amd_coder, amd::coder_from_view); - if(!rc.empty()) { - video.amd.amd_rc_h264 = amd::rc_from_view(rc, 1); - video.amd.amd_rc_hevc = amd::rc_from_view(rc, 0); - } - - std::string usage; - string_f(vars, "amd_usage", usage); - if(!usage.empty()) { - video.amd.amd_usage_h264 = amd::usage_from_view(rc, 1); - video.amd.amd_usage_hevc = amd::usage_from_view(rc, 0); - } - - bool_f(vars, "amd_preanalysis", (bool &)video.amd.amd_preanalysis); - bool_f(vars, "amd_vbaq", (bool &)video.amd.amd_vbaq); - - int_f(vars, "vt_coder", video.vt.vt_coder, vt::coder_from_view); - int_f(vars, "vt_software", video.vt.vt_allow_sw, vt::allow_software_from_view); - int_f(vars, "vt_software", video.vt.vt_require_sw, vt::force_software_from_view); - int_f(vars, "vt_realtime", video.vt.vt_realtime, vt::rt_from_view); - - string_f(vars, "capture", video.capture); - string_f(vars, "encoder", video.encoder); - string_f(vars, "adapter_name", video.adapter_name); - string_f(vars, "output_name", video.output_name); - bool_f(vars, "dwmflush", video.dwmflush); - - path_f(vars, "pkey", nvhttp.pkey); - path_f(vars, "cert", nvhttp.cert); - string_f(vars, "sunshine_name", nvhttp.sunshine_name); - path_f(vars, "log_path", config::sunshine.log_file); - path_f(vars, "file_state", nvhttp.file_state); - - // Must be run after "file_state" - config::sunshine.credentials_file = config::nvhttp.file_state; - path_f(vars, "credentials_file", config::sunshine.credentials_file); - - string_f(vars, "external_ip", nvhttp.external_ip); - list_string_f(vars, "resolutions"s, nvhttp.resolutions); - list_int_f(vars, "fps"s, nvhttp.fps); - list_prep_cmd_f(vars, "global_prep_cmd", config::sunshine.prep_cmds); - - string_f(vars, "audio_sink", audio.sink); - string_f(vars, "virtual_sink", audio.virtual_sink); - - string_restricted_f(vars, "origin_pin_allowed", nvhttp.origin_pin_allowed, { "pc"sv, "lan"sv, "wan"sv }); - string_restricted_f(vars, "origin_web_ui_allowed", nvhttp.origin_web_ui_allowed, { "pc"sv, "lan"sv, "wan"sv }); - - int to = -1; - int_between_f(vars, "ping_timeout", to, { -1, std::numeric_limits::max() }); - if(to != -1) { - stream.ping_timeout = std::chrono::milliseconds(to); - } - - int_between_f(vars, "channels", stream.channels, { 1, std::numeric_limits::max() }); - - path_f(vars, "file_apps", stream.file_apps); - int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 255 }); - - map_int_int_f(vars, "keybindings"s, input.keybindings); - - // This config option will only be used by the UI - // When editing in the config file itself, use "keybindings" - bool map_rightalt_to_win = false; - bool_f(vars, "key_rightalt_to_key_win", map_rightalt_to_win); - - if(map_rightalt_to_win) { - input.keybindings.emplace(0xA5, 0x5B); - } - - to = std::numeric_limits::min(); - int_f(vars, "back_button_timeout", to); - - if(to > std::numeric_limits::min()) { - input.back_button_timeout = std::chrono::milliseconds { to }; - } - - double repeat_frequency { 0 }; - double_between_f(vars, "key_repeat_frequency", repeat_frequency, { 0, std::numeric_limits::max() }); - - if(repeat_frequency > 0) { - config::input.key_repeat_period = std::chrono::duration { 1 / repeat_frequency }; - } - - to = -1; - int_f(vars, "key_repeat_delay", to); - if(to >= 0) { - input.key_repeat_delay = std::chrono::milliseconds { to }; - } - - string_restricted_f(vars, "gamepad"s, input.gamepad, platf::supported_gamepads()); - - bool_f(vars, "mouse", input.mouse); - bool_f(vars, "keyboard", input.keyboard); - bool_f(vars, "controller", input.controller); - - int port = sunshine.port; - int_f(vars, "port"s, port); - sunshine.port = (std::uint16_t)port; - - bool upnp = false; - bool_f(vars, "upnp"s, upnp); - - if(upnp) { - config::sunshine.flags[config::flag::UPNP].flip(); - } - - std::string log_level_string; - string_f(vars, "min_log_level", log_level_string); - - if(!log_level_string.empty()) { - if(log_level_string == "verbose"sv) { - sunshine.min_log_level = 0; - } - else if(log_level_string == "debug"sv) { - sunshine.min_log_level = 1; - } - else if(log_level_string == "info"sv) { - sunshine.min_log_level = 2; - } - else if(log_level_string == "warning"sv) { - sunshine.min_log_level = 3; - } - else if(log_level_string == "error"sv) { - sunshine.min_log_level = 4; - } - else if(log_level_string == "fatal"sv) { - sunshine.min_log_level = 5; - } - else if(log_level_string == "none"sv) { - sunshine.min_log_level = 6; - } - else { - // accept digit directly - auto val = log_level_string[0]; - if(val >= '0' && val < '7') { - sunshine.min_log_level = val - '0'; + return std::make_pair(endl, std::nullopt); } } + + return std::make_pair( + endl, + std::make_pair(to_string(begin, end_name), to_string(begin_val, endl))); } - auto it = vars.find("flags"s); - if(it != std::end(vars)) { - apply_flags(it->second.c_str()); + std::unordered_map + parse_config(const std::string_view &file_content) { + std::unordered_map vars; + + auto pos = std::begin(file_content); + auto end = std::end(file_content); + + while (pos < end) { + // auto newline = std::find_if(pos, end, [](auto ch) { return ch == '\n' || ch == '\r'; }); + TUPLE_2D(endl, var, parse_option(pos, end)); + + pos = endl; + if (pos != end) { + pos += (*pos == '\r') ? 2 : 1; + } + + if (!var) { + continue; + } + + vars.emplace(std::move(*var)); + } + + return vars; + } + + void + string_f(std::unordered_map &vars, const std::string &name, std::string &input) { + auto it = vars.find(name); + if (it == std::end(vars)) { + return; + } + + input = std::move(it->second); vars.erase(it); } - if(sunshine.min_log_level <= 3) { - for(auto &[var, _] : vars) { - std::cout << "Warning: Unrecognized configurable option ["sv << var << ']' << std::endl; + void + string_restricted_f(std::unordered_map &vars, const std::string &name, std::string &input, const std::vector &allowed_vals) { + std::string temp; + string_f(vars, name, temp); + + for (auto &allowed_val : allowed_vals) { + if (temp == allowed_val) { + input = std::move(temp); + return; + } } } -} -int parse(int argc, char *argv[]) { - std::unordered_map cmd_vars; + void + path_f(std::unordered_map &vars, const std::string &name, fs::path &input) { + // appdata needs to be retrieved once only + static auto appdata = platf::appdata(); - for(auto x = 1; x < argc; ++x) { - auto line = argv[x]; + std::string temp; + string_f(vars, name, temp); - if(line == "--help"sv) { - print_help(*argv); - return 1; + if (!temp.empty()) { + input = temp; } - else if(*line == '-') { - if(*(line + 1) == '-') { - sunshine.cmd.name = line + 2; - sunshine.cmd.argc = argc - x - 1; - sunshine.cmd.argv = argv + x + 1; - break; - } - if(apply_flags(line + 1)) { - print_help(*argv); - return -1; - } + if (input.is_relative()) { + input = appdata / input; + } + + auto dir = input; + dir.remove_filename(); + + // Ensure the directories exists + if (!fs::exists(dir)) { + fs::create_directories(dir); + } + } + + void + path_f(std::unordered_map &vars, const std::string &name, std::string &input) { + fs::path temp = input; + + path_f(vars, name, temp); + + input = temp.string(); + } + + void + int_f(std::unordered_map &vars, const std::string &name, int &input) { + auto it = vars.find(name); + + if (it == std::end(vars)) { + return; + } + + std::string_view val = it->second; + + // If value is something like: "756" instead of 756 + if (val.size() >= 2 && val[0] == '"') { + val = val.substr(1, val.size() - 2); + } + + // If that integer is in hexadecimal + if (val.size() >= 2 && val.substr(0, 2) == "0x"sv) { + input = util::from_hex(val.substr(2)); } else { - auto line_end = line + strlen(line); + input = util::from_view(val); + } - auto pos = std::find(line, line_end, '='); - if(pos == line_end) { - sunshine.config_file = line; + vars.erase(it); + } + + void + int_f(std::unordered_map &vars, const std::string &name, std::optional &input) { + auto it = vars.find(name); + + if (it == std::end(vars)) { + return; + } + + std::string_view val = it->second; + + // If value is something like: "756" instead of 756 + if (val.size() >= 2 && val[0] == '"') { + val = val.substr(1, val.size() - 2); + } + + // If that integer is in hexadecimal + if (val.size() >= 2 && val.substr(0, 2) == "0x"sv) { + input = util::from_hex(val.substr(2)); + } + else { + input = util::from_view(val); + } + + vars.erase(it); + } + + template + void + int_f(std::unordered_map &vars, const std::string &name, int &input, F &&f) { + std::string tmp; + string_f(vars, name, tmp); + if (!tmp.empty()) { + input = f(tmp); + } + } + + template + void + int_f(std::unordered_map &vars, const std::string &name, std::optional &input, F &&f) { + std::string tmp; + string_f(vars, name, tmp); + if (!tmp.empty()) { + input = f(tmp); + } + } + + void + int_between_f(std::unordered_map &vars, const std::string &name, int &input, const std::pair &range) { + int temp = input; + + int_f(vars, name, temp); + + TUPLE_2D_REF(lower, upper, range); + if (temp >= lower && temp <= upper) { + input = temp; + } + } + + bool + to_bool(std::string &boolean) { + std::for_each(std::begin(boolean), std::end(boolean), [](char ch) { return (char) std::tolower(ch); }); + + return boolean == "true"sv || + boolean == "yes"sv || + boolean == "enable"sv || + boolean == "enabled"sv || + boolean == "on"sv || + (std::find(std::begin(boolean), std::end(boolean), '1') != std::end(boolean)); + } + + void + bool_f(std::unordered_map &vars, const std::string &name, bool &input) { + std::string tmp; + string_f(vars, name, tmp); + + if (tmp.empty()) { + return; + } + + input = to_bool(tmp); + } + + void + double_f(std::unordered_map &vars, const std::string &name, double &input) { + std::string tmp; + string_f(vars, name, tmp); + + if (tmp.empty()) { + return; + } + + char *c_str_p; + auto val = std::strtod(tmp.c_str(), &c_str_p); + + if (c_str_p == tmp.c_str()) { + return; + } + + input = val; + } + + void + double_between_f(std::unordered_map &vars, const std::string &name, double &input, const std::pair &range) { + double temp = input; + + double_f(vars, name, temp); + + TUPLE_2D_REF(lower, upper, range); + if (temp >= lower && temp <= upper) { + input = temp; + } + } + + void + list_string_f(std::unordered_map &vars, const std::string &name, std::vector &input) { + std::string string; + string_f(vars, name, string); + + if (string.empty()) { + return; + } + + input.clear(); + + auto begin = std::cbegin(string); + if (*begin == '[') { + ++begin; + } + + begin = std::find_if_not(begin, std::cend(string), whitespace); + if (begin == std::cend(string)) { + return; + } + + auto pos = begin; + while (pos < std::cend(string)) { + if (*pos == '[') { + pos = skip_list(pos + 1, std::cend(string)) + 1; + } + else if (*pos == ']') { + break; + } + else if (*pos == ',') { + input.emplace_back(begin, pos); + pos = begin = std::find_if_not(pos + 1, std::cend(string), whitespace); } else { - TUPLE_EL(var, 1, parse_option(line, line_end)); - if(!var) { + ++pos; + } + } + + if (pos != begin) { + input.emplace_back(begin, pos); + } + } + + void + list_prep_cmd_f(std::unordered_map &vars, const std::string &name, std::vector &input) { + std::string string; + string_f(vars, name, string); + + std::stringstream jsonStream; + + // check if string is empty, i.e. when the value doesn't exist in the config file + if (string.empty()) { + return; + } + + // We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it. + jsonStream << "{\"prep_cmd\":" << string << "}"; + + boost::property_tree::ptree jsonTree; + boost::property_tree::read_json(jsonStream, jsonTree); + + for (auto &[_, prep_cmd] : jsonTree.get_child("prep_cmd"s)) { + auto do_cmd = prep_cmd.get("do"s); + auto undo_cmd = prep_cmd.get("undo"s); + + input.emplace_back( + std::move(do_cmd), + std::move(undo_cmd)); + } + } + + void + list_int_f(std::unordered_map &vars, const std::string &name, std::vector &input) { + std::vector list; + list_string_f(vars, name, list); + + for (auto &el : list) { + std::string_view val = el; + + // If value is something like: "756" instead of 756 + if (val.size() >= 2 && val[0] == '"') { + val = val.substr(1, val.size() - 2); + } + + int tmp; + + // If the integer is a hexadecimal + if (val.size() >= 2 && val.substr(0, 2) == "0x"sv) { + tmp = util::from_hex(val.substr(2)); + } + else { + tmp = util::from_view(val); + } + input.emplace_back(tmp); + } + } + + void + map_int_int_f(std::unordered_map &vars, const std::string &name, std::unordered_map &input) { + std::vector list; + list_int_f(vars, name, list); + + // The list needs to be a multiple of 2 + if (list.size() % 2) { + std::cout << "Warning: expected "sv << name << " to have a multiple of two elements --> not "sv << list.size() << std::endl; + return; + } + + int x = 0; + while (x < list.size()) { + auto key = list[x++]; + auto val = list[x++]; + + input.emplace(key, val); + } + } + + int + apply_flags(const char *line) { + int ret = 0; + while (*line != '\0') { + switch (*line) { + case '0': + config::sunshine.flags[config::flag::PIN_STDIN].flip(); + break; + case '1': + config::sunshine.flags[config::flag::FRESH_STATE].flip(); + break; + case '2': + config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE].flip(); + break; + case 'p': + config::sunshine.flags[config::flag::UPNP].flip(); + break; + default: + std::cout << "Warning: Unrecognized flag: ["sv << *line << ']' << std::endl; + ret = -1; + } + + ++line; + } + + return ret; + } + + void + apply_config(std::unordered_map &&vars) { + if (!fs::exists(stream.file_apps.c_str())) { + fs::copy_file(SUNSHINE_ASSETS_DIR "/apps.json", stream.file_apps); + } + + for (auto &[name, val] : vars) { + std::cout << "["sv << name << "] -- ["sv << val << ']' << std::endl; + } + + int_f(vars, "qp", video.qp); + int_f(vars, "min_threads", video.min_threads); + int_between_f(vars, "hevc_mode", video.hevc_mode, { 0, 3 }); + string_f(vars, "sw_preset", video.sw.sw_preset); + string_f(vars, "sw_tune", video.sw.sw_tune); + int_f(vars, "nv_preset", video.nv.nv_preset, nv::preset_from_view); + int_f(vars, "nv_tune", video.nv.nv_tune, nv::tune_from_view); + int_f(vars, "nv_rc", video.nv.nv_rc, nv::rc_from_view); + int_f(vars, "nv_coder", video.nv.nv_coder, nv::coder_from_view); + + int_f(vars, "qsv_preset", video.qsv.qsv_preset, qsv::preset_from_view); + int_f(vars, "qsv_coder", video.qsv.qsv_cavlc, qsv::coder_from_view); + + std::string quality; + string_f(vars, "amd_quality", quality); + if (!quality.empty()) { + video.amd.amd_quality_h264 = amd::quality_from_view(quality, 1); + video.amd.amd_quality_hevc = amd::quality_from_view(quality, 0); + } + + std::string rc; + string_f(vars, "amd_rc", rc); + int_f(vars, "amd_coder", video.amd.amd_coder, amd::coder_from_view); + if (!rc.empty()) { + video.amd.amd_rc_h264 = amd::rc_from_view(rc, 1); + video.amd.amd_rc_hevc = amd::rc_from_view(rc, 0); + } + + std::string usage; + string_f(vars, "amd_usage", usage); + if (!usage.empty()) { + video.amd.amd_usage_h264 = amd::usage_from_view(rc, 1); + video.amd.amd_usage_hevc = amd::usage_from_view(rc, 0); + } + + bool_f(vars, "amd_preanalysis", (bool &) video.amd.amd_preanalysis); + bool_f(vars, "amd_vbaq", (bool &) video.amd.amd_vbaq); + + int_f(vars, "vt_coder", video.vt.vt_coder, vt::coder_from_view); + int_f(vars, "vt_software", video.vt.vt_allow_sw, vt::allow_software_from_view); + int_f(vars, "vt_software", video.vt.vt_require_sw, vt::force_software_from_view); + int_f(vars, "vt_realtime", video.vt.vt_realtime, vt::rt_from_view); + + string_f(vars, "capture", video.capture); + string_f(vars, "encoder", video.encoder); + string_f(vars, "adapter_name", video.adapter_name); + string_f(vars, "output_name", video.output_name); + bool_f(vars, "dwmflush", video.dwmflush); + + path_f(vars, "pkey", nvhttp.pkey); + path_f(vars, "cert", nvhttp.cert); + string_f(vars, "sunshine_name", nvhttp.sunshine_name); + path_f(vars, "log_path", config::sunshine.log_file); + path_f(vars, "file_state", nvhttp.file_state); + + // Must be run after "file_state" + config::sunshine.credentials_file = config::nvhttp.file_state; + path_f(vars, "credentials_file", config::sunshine.credentials_file); + + string_f(vars, "external_ip", nvhttp.external_ip); + list_string_f(vars, "resolutions"s, nvhttp.resolutions); + list_int_f(vars, "fps"s, nvhttp.fps); + list_prep_cmd_f(vars, "global_prep_cmd", config::sunshine.prep_cmds); + + string_f(vars, "audio_sink", audio.sink); + string_f(vars, "virtual_sink", audio.virtual_sink); + + string_restricted_f(vars, "origin_pin_allowed", nvhttp.origin_pin_allowed, { "pc"sv, "lan"sv, "wan"sv }); + string_restricted_f(vars, "origin_web_ui_allowed", nvhttp.origin_web_ui_allowed, { "pc"sv, "lan"sv, "wan"sv }); + + int to = -1; + int_between_f(vars, "ping_timeout", to, { -1, std::numeric_limits::max() }); + if (to != -1) { + stream.ping_timeout = std::chrono::milliseconds(to); + } + + int_between_f(vars, "channels", stream.channels, { 1, std::numeric_limits::max() }); + + path_f(vars, "file_apps", stream.file_apps); + int_between_f(vars, "fec_percentage", stream.fec_percentage, { 1, 255 }); + + map_int_int_f(vars, "keybindings"s, input.keybindings); + + // This config option will only be used by the UI + // When editing in the config file itself, use "keybindings" + bool map_rightalt_to_win = false; + bool_f(vars, "key_rightalt_to_key_win", map_rightalt_to_win); + + if (map_rightalt_to_win) { + input.keybindings.emplace(0xA5, 0x5B); + } + + to = std::numeric_limits::min(); + int_f(vars, "back_button_timeout", to); + + if (to > std::numeric_limits::min()) { + input.back_button_timeout = std::chrono::milliseconds { to }; + } + + double repeat_frequency { 0 }; + double_between_f(vars, "key_repeat_frequency", repeat_frequency, { 0, std::numeric_limits::max() }); + + if (repeat_frequency > 0) { + config::input.key_repeat_period = std::chrono::duration { 1 / repeat_frequency }; + } + + to = -1; + int_f(vars, "key_repeat_delay", to); + if (to >= 0) { + input.key_repeat_delay = std::chrono::milliseconds { to }; + } + + string_restricted_f(vars, "gamepad"s, input.gamepad, platf::supported_gamepads()); + + bool_f(vars, "mouse", input.mouse); + bool_f(vars, "keyboard", input.keyboard); + bool_f(vars, "controller", input.controller); + + int port = sunshine.port; + int_f(vars, "port"s, port); + sunshine.port = (std::uint16_t) port; + + bool upnp = false; + bool_f(vars, "upnp"s, upnp); + + if (upnp) { + config::sunshine.flags[config::flag::UPNP].flip(); + } + + std::string log_level_string; + string_f(vars, "min_log_level", log_level_string); + + if (!log_level_string.empty()) { + if (log_level_string == "verbose"sv) { + sunshine.min_log_level = 0; + } + else if (log_level_string == "debug"sv) { + sunshine.min_log_level = 1; + } + else if (log_level_string == "info"sv) { + sunshine.min_log_level = 2; + } + else if (log_level_string == "warning"sv) { + sunshine.min_log_level = 3; + } + else if (log_level_string == "error"sv) { + sunshine.min_log_level = 4; + } + else if (log_level_string == "fatal"sv) { + sunshine.min_log_level = 5; + } + else if (log_level_string == "none"sv) { + sunshine.min_log_level = 6; + } + else { + // accept digit directly + auto val = log_level_string[0]; + if (val >= '0' && val < '7') { + sunshine.min_log_level = val - '0'; + } + } + } + + auto it = vars.find("flags"s); + if (it != std::end(vars)) { + apply_flags(it->second.c_str()); + + vars.erase(it); + } + + if (sunshine.min_log_level <= 3) { + for (auto &[var, _] : vars) { + std::cout << "Warning: Unrecognized configurable option ["sv << var << ']' << std::endl; + } + } + } + + int + parse(int argc, char *argv[]) { + std::unordered_map cmd_vars; + + for (auto x = 1; x < argc; ++x) { + auto line = argv[x]; + + if (line == "--help"sv) { + print_help(*argv); + return 1; + } + else if (*line == '-') { + if (*(line + 1) == '-') { + sunshine.cmd.name = line + 2; + sunshine.cmd.argc = argc - x - 1; + sunshine.cmd.argv = argv + x + 1; + + break; + } + if (apply_flags(line + 1)) { print_help(*argv); return -1; } + } + else { + auto line_end = line + strlen(line); - TUPLE_EL_REF(name, 0, *var); - - auto it = cmd_vars.find(name); - if(it != std::end(cmd_vars)) { - cmd_vars.erase(it); + auto pos = std::find(line, line_end, '='); + if (pos == line_end) { + sunshine.config_file = line; } + else { + TUPLE_EL(var, 1, parse_option(line, line_end)); + if (!var) { + print_help(*argv); + return -1; + } - cmd_vars.emplace(std::move(*var)); + TUPLE_EL_REF(name, 0, *var); + + auto it = cmd_vars.find(name); + if (it != std::end(cmd_vars)) { + cmd_vars.erase(it); + } + + cmd_vars.emplace(std::move(*var)); + } } } + + // create appdata folder if it does not exist + if (!boost::filesystem::exists(platf::appdata().string())) { + boost::filesystem::create_directory(platf::appdata().string()); + } + + // create config file if it does not exist + if (!fs::exists(sunshine.config_file)) { + std::ofstream { sunshine.config_file }; // create empty config file + } + + auto vars = parse_config(read_file(sunshine.config_file.c_str())); + + for (auto &[name, value] : cmd_vars) { + vars.insert_or_assign(std::move(name), std::move(value)); + } + + apply_config(std::move(vars)); + + return 0; } - - // create appdata folder if it does not exist - if(!boost::filesystem::exists(platf::appdata().string())) { - boost::filesystem::create_directory(platf::appdata().string()); - } - - // create config file if it does not exist - if(!fs::exists(sunshine.config_file)) { - std::ofstream { sunshine.config_file }; // create empty config file - } - - auto vars = parse_config(read_file(sunshine.config_file.c_str())); - - for(auto &[name, value] : cmd_vars) { - vars.insert_or_assign(std::move(name), std::move(value)); - } - - apply_config(std::move(vars)); - - return 0; -} -} // namespace config +} // namespace config diff --git a/src/config.h b/src/config.h index 28871833..fc77748b 100644 --- a/src/config.h +++ b/src/config.h @@ -9,153 +9,157 @@ #include namespace config { -struct video_t { - // ffmpeg params - int qp; // higher == more compression and less quality + struct video_t { + // ffmpeg params + int qp; // higher == more compression and less quality - int hevc_mode; + int hevc_mode; - int min_threads; // Minimum number of threads/slices for CPU encoding - struct { - std::string sw_preset; - std::string sw_tune; - } sw; + int min_threads; // Minimum number of threads/slices for CPU encoding + struct { + std::string sw_preset; + std::string sw_tune; + } sw; - struct { - std::optional nv_preset; - std::optional nv_tune; - std::optional nv_rc; - int nv_coder; - } nv; + struct { + std::optional nv_preset; + std::optional nv_tune; + std::optional nv_rc; + int nv_coder; + } nv; - struct { - std::optional qsv_preset; - std::optional qsv_cavlc; - } qsv; + struct { + std::optional qsv_preset; + std::optional qsv_cavlc; + } qsv; - struct { - std::optional amd_quality_h264; - std::optional amd_quality_hevc; - std::optional amd_rc_h264; - std::optional amd_rc_hevc; - std::optional amd_usage_h264; - std::optional amd_usage_hevc; - std::optional amd_preanalysis; - std::optional amd_vbaq; - int amd_coder; - } amd; + struct { + std::optional amd_quality_h264; + std::optional amd_quality_hevc; + std::optional amd_rc_h264; + std::optional amd_rc_hevc; + std::optional amd_usage_h264; + std::optional amd_usage_hevc; + std::optional amd_preanalysis; + std::optional amd_vbaq; + int amd_coder; + } amd; - struct { - int vt_allow_sw; - int vt_require_sw; - int vt_realtime; - int vt_coder; - } vt; + struct { + int vt_allow_sw; + int vt_require_sw; + int vt_realtime; + int vt_coder; + } vt; - std::string capture; - std::string encoder; - std::string adapter_name; - std::string output_name; - bool dwmflush; -}; + std::string capture; + std::string encoder; + std::string adapter_name; + std::string output_name; + bool dwmflush; + }; -struct audio_t { - std::string sink; - std::string virtual_sink; -}; + struct audio_t { + std::string sink; + std::string virtual_sink; + }; -struct stream_t { - std::chrono::milliseconds ping_timeout; + struct stream_t { + std::chrono::milliseconds ping_timeout; - std::string file_apps; + std::string file_apps; - int fec_percentage; + int fec_percentage; - // max unique instances of video and audio streams - int channels; -}; + // max unique instances of video and audio streams + int channels; + }; -struct nvhttp_t { - // Could be any of the following values: - // pc|lan|wan - std::string origin_pin_allowed; - std::string origin_web_ui_allowed; + struct nvhttp_t { + // Could be any of the following values: + // pc|lan|wan + std::string origin_pin_allowed; + std::string origin_web_ui_allowed; - std::string pkey; // must be 2048 bits - std::string cert; // must be signed with a key of 2048 bits + std::string pkey; // must be 2048 bits + std::string cert; // must be signed with a key of 2048 bits - std::string sunshine_name; + std::string sunshine_name; - std::string file_state; + std::string file_state; - std::string external_ip; - std::vector resolutions; - std::vector fps; -}; + std::string external_ip; + std::vector resolutions; + std::vector fps; + }; -struct input_t { - std::unordered_map keybindings; + struct input_t { + std::unordered_map keybindings; - std::chrono::milliseconds back_button_timeout; - std::chrono::milliseconds key_repeat_delay; - std::chrono::duration key_repeat_period; + std::chrono::milliseconds back_button_timeout; + std::chrono::milliseconds key_repeat_delay; + std::chrono::duration key_repeat_period; - std::string gamepad; + std::string gamepad; - bool keyboard; - bool mouse; - bool controller; -}; + bool keyboard; + bool mouse; + bool controller; + }; -namespace flag { -enum flag_e : std::size_t { - PIN_STDIN = 0, // Read PIN from stdin instead of http - FRESH_STATE, // Do not load or save state - FORCE_VIDEO_HEADER_REPLACE, // force replacing headers inside video data - UPNP, // Try Universal Plug 'n Play - CONST_PIN, // Use "universal" pin - FLAG_SIZE -}; -} + namespace flag { + enum flag_e : std::size_t { + PIN_STDIN = 0, // Read PIN from stdin instead of http + FRESH_STATE, // Do not load or save state + FORCE_VIDEO_HEADER_REPLACE, // force replacing headers inside video data + UPNP, // Try Universal Plug 'n Play + CONST_PIN, // Use "universal" pin + FLAG_SIZE + }; + } -struct prep_cmd_t { - prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd) : do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)) {} - explicit prep_cmd_t(std::string &&do_cmd) : do_cmd(std::move(do_cmd)) {} - std::string do_cmd; - std::string undo_cmd; -}; + struct prep_cmd_t { + prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd): + do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)) {} + explicit prep_cmd_t(std::string &&do_cmd): + do_cmd(std::move(do_cmd)) {} + std::string do_cmd; + std::string undo_cmd; + }; -struct sunshine_t { - int min_log_level; - std::bitset flags; - std::string credentials_file; + struct sunshine_t { + int min_log_level; + std::bitset flags; + std::string credentials_file; - std::string username; - std::string password; - std::string salt; + std::string username; + std::string password; + std::string salt; - std::string config_file; + std::string config_file; - struct cmd_t { - std::string name; - int argc; - char **argv; - } cmd; + struct cmd_t { + std::string name; + int argc; + char **argv; + } cmd; - std::uint16_t port; - std::string log_file; + std::uint16_t port; + std::string log_file; - std::vector prep_cmds; -}; + std::vector prep_cmds; + }; -extern video_t video; -extern audio_t audio; -extern stream_t stream; -extern nvhttp_t nvhttp; -extern input_t input; -extern sunshine_t sunshine; + extern video_t video; + extern audio_t audio; + extern stream_t stream; + extern nvhttp_t nvhttp; + extern input_t input; + extern sunshine_t sunshine; -int parse(int argc, char *argv[]); -std::unordered_map parse_config(const std::string_view &file_content); -} // namespace config + int + parse(int argc, char *argv[]); + std::unordered_map + parse_config(const std::string_view &file_content); +} // namespace config #endif diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 5f54aebb..c7a2a8b3 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -38,726 +38,756 @@ using namespace std::literals; namespace confighttp { -namespace fs = std::filesystem; -namespace pt = boost::property_tree; + namespace fs = std::filesystem; + namespace pt = boost::property_tree; -using https_server_t = SimpleWeb::Server; + using https_server_t = SimpleWeb::Server; -using args_t = SimpleWeb::CaseInsensitiveMultimap; -using resp_https_t = std::shared_ptr::Response>; -using req_https_t = std::shared_ptr::Request>; + using args_t = SimpleWeb::CaseInsensitiveMultimap; + using resp_https_t = std::shared_ptr::Response>; + using req_https_t = std::shared_ptr::Request>; -enum class op_e { - ADD, - REMOVE -}; - -void print_req(const req_https_t &request) { - BOOST_LOG(debug) << "METHOD :: "sv << request->method; - BOOST_LOG(debug) << "DESTINATION :: "sv << request->path; - - for(auto &[name, val] : request->header) { - BOOST_LOG(debug) << name << " -- " << (name == "Authorization" ? "CREDENTIALS REDACTED" : val); - } - - BOOST_LOG(debug) << " [--] "sv; - - for(auto &[name, val] : request->parse_query_string()) { - BOOST_LOG(debug) << name << " -- " << val; - } - - BOOST_LOG(debug) << " [--] "sv; -} - -void send_unauthorized(resp_https_t response, req_https_t request) { - auto address = request->remote_endpoint().address().to_string(); - BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv; - const SimpleWeb::CaseInsensitiveMultimap headers { - { "WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")" } + enum class op_e { + ADD, + REMOVE }; - response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers); -} -void send_redirect(resp_https_t response, req_https_t request, const char *path) { - auto address = request->remote_endpoint().address().to_string(); - BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv; - const SimpleWeb::CaseInsensitiveMultimap headers { - { "Location", path } - }; - response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers); -} + void + print_req(const req_https_t &request) { + BOOST_LOG(debug) << "METHOD :: "sv << request->method; + BOOST_LOG(debug) << "DESTINATION :: "sv << request->path; -bool authenticate(resp_https_t response, req_https_t request) { - auto address = request->remote_endpoint().address().to_string(); - auto ip_type = net::from_address(address); - - if(ip_type > http::origin_web_ui_allowed) { - BOOST_LOG(info) << "Web UI: ["sv << address << "] -- denied"sv; - response->write(SimpleWeb::StatusCode::client_error_forbidden); - return false; - } - - // If credentials are shown, redirect the user to a /welcome page - if(config::sunshine.username.empty()) { - send_redirect(response, request, "/welcome"); - return false; - } - - auto fg = util::fail_guard([&]() { - send_unauthorized(response, request); - }); - - auto auth = request->header.find("authorization"); - if(auth == request->header.end()) { - return false; - } - - auto &rawAuth = auth->second; - auto authData = SimpleWeb::Crypto::Base64::decode(rawAuth.substr("Basic "sv.length())); - - int index = authData.find(':'); - if(index >= authData.size() - 1) { - return false; - } - - auto username = authData.substr(0, index); - auto password = authData.substr(index + 1); - auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); - - if(username != config::sunshine.username || hash != config::sunshine.password) { - return false; - } - - fg.disable(); - return true; -} - -void not_found(resp_https_t response, req_https_t request) { - pt::ptree tree; - tree.put("root..status_code", 404); - - std::ostringstream data; - - pt::write_xml(data, tree); - response->write(data.str()); - - *response << "HTTP/1.1 404 NOT FOUND\r\n" - << data.str(); -} - -// todo - combine these functions into a single function that accepts the page, i.e "index", "pin", "apps" -void getIndexPage(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; - - print_req(request); - - std::string header = read_file(WEB_DIR "header.html"); - std::string content = read_file(WEB_DIR "index.html"); - response->write(header + content); -} - -void getPinPage(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; - - print_req(request); - - std::string header = read_file(WEB_DIR "header.html"); - std::string content = read_file(WEB_DIR "pin.html"); - response->write(header + content); -} - -void getAppsPage(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; - - print_req(request); - - SimpleWeb::CaseInsensitiveMultimap headers; - headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/"); - - std::string header = read_file(WEB_DIR "header.html"); - std::string content = read_file(WEB_DIR "apps.html"); - response->write(header + content, headers); -} - -void getClientsPage(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; - - print_req(request); - - std::string header = read_file(WEB_DIR "header.html"); - std::string content = read_file(WEB_DIR "clients.html"); - response->write(header + content); -} - -void getConfigPage(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; - - print_req(request); - - std::string header = read_file(WEB_DIR "header.html"); - std::string content = read_file(WEB_DIR "config.html"); - response->write(header + content); -} - -void getPasswordPage(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; - - print_req(request); - - std::string header = read_file(WEB_DIR "header.html"); - std::string content = read_file(WEB_DIR "password.html"); - response->write(header + content); -} - -void getWelcomePage(resp_https_t response, req_https_t request) { - print_req(request); - if(!config::sunshine.username.empty()) { - send_redirect(response, request, "/"); - return; - } - std::string header = read_file(WEB_DIR "header-no-nav.html"); - std::string content = read_file(WEB_DIR "welcome.html"); - response->write(header + content); -} - -void getTroubleshootingPage(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; - - print_req(request); - - std::string header = read_file(WEB_DIR "header.html"); - std::string content = read_file(WEB_DIR "troubleshooting.html"); - response->write(header + content); -} - -void getFaviconImage(resp_https_t response, req_https_t request) { - // todo - combine function with getSunshineLogoImage and possibly getNodeModules - // todo - use mime_types map - print_req(request); - - std::ifstream in(WEB_DIR "images/favicon.ico", std::ios::binary); - SimpleWeb::CaseInsensitiveMultimap headers; - headers.emplace("Content-Type", "image/x-icon"); - response->write(SimpleWeb::StatusCode::success_ok, in, headers); -} - -void getSunshineLogoImage(resp_https_t response, req_https_t request) { - // todo - combine function with getFaviconImage and possibly getNodeModules - // todo - use mime_types map - print_req(request); - - std::ifstream in(WEB_DIR "images/logo-sunshine-45.png", std::ios::binary); - SimpleWeb::CaseInsensitiveMultimap headers; - headers.emplace("Content-Type", "image/png"); - response->write(SimpleWeb::StatusCode::success_ok, in, headers); -} - -bool isChildPath(fs::path const &base, fs::path const &query) { - auto relPath = fs::relative(base, query); - return *(relPath.begin()) != fs::path(".."); -} - -void getNodeModules(resp_https_t response, req_https_t request) { - print_req(request); - fs::path webDirPath(WEB_DIR); - fs::path nodeModulesPath(webDirPath / "node_modules"); - - // .relative_path is needed to shed any leading slash that might exist in the request path - auto filePath = fs::weakly_canonical(webDirPath / fs::path(request->path).relative_path()); - - // Don't do anything if file does not exist or is outside the node_modules directory - if(!isChildPath(filePath, nodeModulesPath)) { - BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the node_modules folder"; - response->write(SimpleWeb::StatusCode::client_error_bad_request, "Bad Request"); - } - else if(!fs::exists(filePath)) { - response->write(SimpleWeb::StatusCode::client_error_not_found); - } - else { - auto relPath = fs::relative(filePath, webDirPath); - // get the mime type from the file extension mime_types map - // remove the leading period from the extension - auto mimeType = mime_types.find(relPath.extension().string().substr(1)); - // check if the extension is in the map at the x position - if(mimeType != mime_types.end()) { - // if it is, set the content type to the mime type - SimpleWeb::CaseInsensitiveMultimap headers; - headers.emplace("Content-Type", mimeType->second); - std::ifstream in(filePath.string(), std::ios::binary); - response->write(SimpleWeb::StatusCode::success_ok, in, headers); + for (auto &[name, val] : request->header) { + BOOST_LOG(debug) << name << " -- " << (name == "Authorization" ? "CREDENTIALS REDACTED" : val); } - // do not return any file if the type is not in the map + + BOOST_LOG(debug) << " [--] "sv; + + for (auto &[name, val] : request->parse_query_string()) { + BOOST_LOG(debug) << name << " -- " << val; + } + + BOOST_LOG(debug) << " [--] "sv; } -} -void getApps(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; + void + send_unauthorized(resp_https_t response, req_https_t request) { + auto address = request->remote_endpoint().address().to_string(); + BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv; + const SimpleWeb::CaseInsensitiveMultimap headers { + { "WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")" } + }; + response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers); + } - print_req(request); + void + send_redirect(resp_https_t response, req_https_t request, const char *path) { + auto address = request->remote_endpoint().address().to_string(); + BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv; + const SimpleWeb::CaseInsensitiveMultimap headers { + { "Location", path } + }; + response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers); + } - std::string content = read_file(config::stream.file_apps.c_str()); - response->write(content); -} + bool + authenticate(resp_https_t response, req_https_t request) { + auto address = request->remote_endpoint().address().to_string(); + auto ip_type = net::from_address(address); -void getLogs(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; + if (ip_type > http::origin_web_ui_allowed) { + BOOST_LOG(info) << "Web UI: ["sv << address << "] -- denied"sv; + response->write(SimpleWeb::StatusCode::client_error_forbidden); + return false; + } - print_req(request); + // If credentials are shown, redirect the user to a /welcome page + if (config::sunshine.username.empty()) { + send_redirect(response, request, "/welcome"); + return false; + } - std::string content = read_file(config::sunshine.log_file.c_str()); - SimpleWeb::CaseInsensitiveMultimap headers; - headers.emplace("Content-Type", "text/plain"); - response->write(SimpleWeb::StatusCode::success_ok, content, headers); -} + auto fg = util::fail_guard([&]() { + send_unauthorized(response, request); + }); -void saveApp(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; + auto auth = request->header.find("authorization"); + if (auth == request->header.end()) { + return false; + } - print_req(request); + auto &rawAuth = auth->second; + auto authData = SimpleWeb::Crypto::Base64::decode(rawAuth.substr("Basic "sv.length())); - std::stringstream ss; - ss << request->content.rdbuf(); + int index = authData.find(':'); + if (index >= authData.size() - 1) { + return false; + } + + auto username = authData.substr(0, index); + auto password = authData.substr(index + 1); + auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); + + if (username != config::sunshine.username || hash != config::sunshine.password) { + return false; + } + + fg.disable(); + return true; + } + + void + not_found(resp_https_t response, req_https_t request) { + pt::ptree tree; + tree.put("root..status_code", 404); - pt::ptree outputTree; - auto g = util::fail_guard([&]() { std::ostringstream data; - pt::write_json(data, outputTree); + pt::write_xml(data, tree); response->write(data.str()); - }); - pt::ptree inputTree, fileTree; + *response << "HTTP/1.1 404 NOT FOUND\r\n" + << data.str(); + } - BOOST_LOG(fatal) << config::stream.file_apps; - try { - // TODO: Input Validation - pt::read_json(ss, inputTree); - pt::read_json(config::stream.file_apps, fileTree); + // todo - combine these functions into a single function that accepts the page, i.e "index", "pin", "apps" + void + getIndexPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; - if(inputTree.get_child("prep-cmd").empty()) { - inputTree.erase("prep-cmd"); + print_req(request); + + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "index.html"); + response->write(header + content); + } + + void + getPinPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "pin.html"); + response->write(header + content); + } + + void + getAppsPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/"); + + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "apps.html"); + response->write(header + content, headers); + } + + void + getClientsPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "clients.html"); + response->write(header + content); + } + + void + getConfigPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "config.html"); + response->write(header + content); + } + + void + getPasswordPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "password.html"); + response->write(header + content); + } + + void + getWelcomePage(resp_https_t response, req_https_t request) { + print_req(request); + if (!config::sunshine.username.empty()) { + send_redirect(response, request, "/"); + return; } + std::string header = read_file(WEB_DIR "header-no-nav.html"); + std::string content = read_file(WEB_DIR "welcome.html"); + response->write(header + content); + } - if(inputTree.get_child("detached").empty()) { - inputTree.erase("detached"); + void + getTroubleshootingPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + std::string header = read_file(WEB_DIR "header.html"); + std::string content = read_file(WEB_DIR "troubleshooting.html"); + response->write(header + content); + } + + void + getFaviconImage(resp_https_t response, req_https_t request) { + // todo - combine function with getSunshineLogoImage and possibly getNodeModules + // todo - use mime_types map + print_req(request); + + std::ifstream in(WEB_DIR "images/favicon.ico", std::ios::binary); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "image/x-icon"); + response->write(SimpleWeb::StatusCode::success_ok, in, headers); + } + + void + getSunshineLogoImage(resp_https_t response, req_https_t request) { + // todo - combine function with getFaviconImage and possibly getNodeModules + // todo - use mime_types map + print_req(request); + + std::ifstream in(WEB_DIR "images/logo-sunshine-45.png", std::ios::binary); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "image/png"); + response->write(SimpleWeb::StatusCode::success_ok, in, headers); + } + + bool + isChildPath(fs::path const &base, fs::path const &query) { + auto relPath = fs::relative(base, query); + return *(relPath.begin()) != fs::path(".."); + } + + void + getNodeModules(resp_https_t response, req_https_t request) { + print_req(request); + fs::path webDirPath(WEB_DIR); + fs::path nodeModulesPath(webDirPath / "node_modules"); + + // .relative_path is needed to shed any leading slash that might exist in the request path + auto filePath = fs::weakly_canonical(webDirPath / fs::path(request->path).relative_path()); + + // Don't do anything if file does not exist or is outside the node_modules directory + if (!isChildPath(filePath, nodeModulesPath)) { + BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the node_modules folder"; + response->write(SimpleWeb::StatusCode::client_error_bad_request, "Bad Request"); } - - auto &apps_node = fileTree.get_child("apps"s); - int index = inputTree.get("index"); - - inputTree.erase("index"); - - if(index == -1) { - apps_node.push_back(std::make_pair("", inputTree)); + else if (!fs::exists(filePath)) { + response->write(SimpleWeb::StatusCode::client_error_not_found); } else { - // Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick - pt::ptree newApps; - int i = 0; - for(const auto &kv : apps_node) { - if(i == index) { - newApps.push_back(std::make_pair("", inputTree)); - } - else { - newApps.push_back(std::make_pair("", kv.second)); - } - i++; + auto relPath = fs::relative(filePath, webDirPath); + // get the mime type from the file extension mime_types map + // remove the leading period from the extension + auto mimeType = mime_types.find(relPath.extension().string().substr(1)); + // check if the extension is in the map at the x position + if (mimeType != mime_types.end()) { + // if it is, set the content type to the mime type + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", mimeType->second); + std::ifstream in(filePath.string(), std::ios::binary); + response->write(SimpleWeb::StatusCode::success_ok, in, headers); } - fileTree.erase("apps"); - fileTree.push_back(std::make_pair("apps", newApps)); + // do not return any file if the type is not in the map } - pt::write_json(config::stream.file_apps, fileTree); - } - catch(std::exception &e) { - BOOST_LOG(warning) << "SaveApp: "sv << e.what(); - - outputTree.put("status", "false"); - outputTree.put("error", "Invalid Input JSON"); - return; } - outputTree.put("status", "true"); - proc::refresh(config::stream.file_apps); -} + void + getApps(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; -void deleteApp(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; + print_req(request); - print_req(request); + std::string content = read_file(config::stream.file_apps.c_str()); + response->write(content); + } - pt::ptree outputTree; - auto g = util::fail_guard([&]() { - std::ostringstream data; + void + getLogs(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; - pt::write_json(data, outputTree); - response->write(data.str()); - }); - pt::ptree fileTree; - try { - pt::read_json(config::stream.file_apps, fileTree); - auto &apps_node = fileTree.get_child("apps"s); - int index = stoi(request->path_match[1]); + print_req(request); - if(index < 0) { - outputTree.put("status", "false"); - outputTree.put("error", "Invalid Index"); - return; - } - else { - // Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick - pt::ptree newApps; - int i = 0; - for(const auto &kv : apps_node) { - if(i++ != index) { - newApps.push_back(std::make_pair("", kv.second)); - } + std::string content = read_file(config::sunshine.log_file.c_str()); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "text/plain"); + response->write(SimpleWeb::StatusCode::success_ok, content, headers); + } + + void + saveApp(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + std::stringstream ss; + ss << request->content.rdbuf(); + + pt::ptree outputTree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + pt::ptree inputTree, fileTree; + + BOOST_LOG(fatal) << config::stream.file_apps; + try { + // TODO: Input Validation + pt::read_json(ss, inputTree); + pt::read_json(config::stream.file_apps, fileTree); + + if (inputTree.get_child("prep-cmd").empty()) { + inputTree.erase("prep-cmd"); } - fileTree.erase("apps"); - fileTree.push_back(std::make_pair("apps", newApps)); - } - pt::write_json(config::stream.file_apps, fileTree); - } - catch(std::exception &e) { - BOOST_LOG(warning) << "DeleteApp: "sv << e.what(); - outputTree.put("status", "false"); - outputTree.put("error", "Invalid File JSON"); - return; - } - outputTree.put("status", "true"); - proc::refresh(config::stream.file_apps); -} + if (inputTree.get_child("detached").empty()) { + inputTree.erase("detached"); + } -void uploadCover(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; + auto &apps_node = fileTree.get_child("apps"s); + int index = inputTree.get("index"); - std::stringstream ss; - std::stringstream configStream; - ss << request->content.rdbuf(); - pt::ptree outputTree; - auto g = util::fail_guard([&]() { - std::ostringstream data; + inputTree.erase("index"); - SimpleWeb::StatusCode code = SimpleWeb::StatusCode::success_ok; - if(outputTree.get_child_optional("error").has_value()) { - code = SimpleWeb::StatusCode::client_error_bad_request; - } - - pt::write_json(data, outputTree); - response->write(code, data.str()); - }); - pt::ptree inputTree; - try { - pt::read_json(ss, inputTree); - } - catch(std::exception &e) { - BOOST_LOG(warning) << "UploadCover: "sv << e.what(); - outputTree.put("status", "false"); - outputTree.put("error", e.what()); - return; - } - - auto key = inputTree.get("key", ""); - if(key.empty()) { - outputTree.put("error", "Cover key is required"); - return; - } - auto url = inputTree.get("url", ""); - - const std::string coverdir = platf::appdata().string() + "/covers/"; - if(!boost::filesystem::exists(coverdir)) { - boost::filesystem::create_directory(coverdir); - } - - std::basic_string path = coverdir + http::url_escape(key) + ".png"; - if(!url.empty()) { - if(http::url_get_host(url) != "images.igdb.com") { - outputTree.put("error", "Only images.igdb.com is allowed"); - return; - } - if(!http::download_file(url, path)) { - outputTree.put("error", "Failed to download cover"); - return; - } - } - else { - auto data = SimpleWeb::Crypto::Base64::decode(inputTree.get("data")); - - std::ofstream imgfile(path); - imgfile.write(data.data(), (int)data.size()); - } - outputTree.put("path", path); -} - -void getConfig(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; - - print_req(request); - - pt::ptree outputTree; - auto g = util::fail_guard([&]() { - std::ostringstream data; - - pt::write_json(data, outputTree); - response->write(data.str()); - }); - - outputTree.put("status", "true"); - outputTree.put("platform", SUNSHINE_PLATFORM); - outputTree.put("version", PROJECT_VER); - outputTree.put("restart_supported", platf::restart_supported()); - - auto vars = config::parse_config(read_file(config::sunshine.config_file.c_str())); - - for(auto &[name, value] : vars) { - outputTree.put(std::move(name), std::move(value)); - } -} - -void saveConfig(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; - - print_req(request); - - std::stringstream ss; - std::stringstream configStream; - ss << request->content.rdbuf(); - pt::ptree outputTree; - auto g = util::fail_guard([&]() { - std::ostringstream data; - - pt::write_json(data, outputTree); - response->write(data.str()); - }); - pt::ptree inputTree; - try { - // TODO: Input Validation - pt::read_json(ss, inputTree); - for(const auto &kv : inputTree) { - std::string value = inputTree.get(kv.first); - if(value.length() == 0 || value.compare("null") == 0) continue; - - configStream << kv.first << " = " << value << std::endl; - } - write_file(config::sunshine.config_file.c_str(), configStream.str()); - } - catch(std::exception &e) { - BOOST_LOG(warning) << "SaveConfig: "sv << e.what(); - outputTree.put("status", "false"); - outputTree.put("error", e.what()); - return; - } -} - -void restart(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; - - print_req(request); - - std::stringstream ss; - std::stringstream configStream; - ss << request->content.rdbuf(); - pt::ptree outputTree; - auto g = util::fail_guard([&]() { - std::ostringstream data; - - pt::write_json(data, outputTree); - response->write(data.str()); - }); - - if(!platf::restart_supported()) { - outputTree.put("status", false); - outputTree.put("error", "Restart is not currently supported on this platform"); - return; - } - - if(!platf::restart()) { - outputTree.put("status", false); - outputTree.put("error", "Restart failed"); - return; - } - - outputTree.put("status", true); -} - -void savePassword(resp_https_t response, req_https_t request) { - if(!config::sunshine.username.empty() && !authenticate(response, request)) return; - - print_req(request); - - std::stringstream ss; - std::stringstream configStream; - ss << request->content.rdbuf(); - - pt::ptree inputTree, outputTree; - - auto g = util::fail_guard([&]() { - std::ostringstream data; - pt::write_json(data, outputTree); - response->write(data.str()); - }); - - try { - // TODO: Input Validation - pt::read_json(ss, inputTree); - auto username = inputTree.count("currentUsername") > 0 ? inputTree.get("currentUsername") : ""; - auto newUsername = inputTree.get("newUsername"); - auto password = inputTree.count("currentPassword") > 0 ? inputTree.get("currentPassword") : ""; - auto newPassword = inputTree.count("newPassword") > 0 ? inputTree.get("newPassword") : ""; - auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get("confirmNewPassword") : ""; - if(newUsername.length() == 0) newUsername = username; - if(newUsername.length() == 0) { - outputTree.put("status", false); - outputTree.put("error", "Invalid Username"); - } - else { - auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); - if(config::sunshine.username.empty() || (username == config::sunshine.username && hash == config::sunshine.password)) { - if(newPassword.empty() || newPassword != confirmPassword) { - outputTree.put("status", false); - outputTree.put("error", "Password Mismatch"); - } - else { - http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword); - http::reload_user_creds(config::sunshine.credentials_file); - outputTree.put("status", true); - } + if (index == -1) { + apps_node.push_back(std::make_pair("", inputTree)); } else { - outputTree.put("status", false); - outputTree.put("error", "Invalid Current Credentials"); + // Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick + pt::ptree newApps; + int i = 0; + for (const auto &kv : apps_node) { + if (i == index) { + newApps.push_back(std::make_pair("", inputTree)); + } + else { + newApps.push_back(std::make_pair("", kv.second)); + } + i++; + } + fileTree.erase("apps"); + fileTree.push_back(std::make_pair("apps", newApps)); } + pt::write_json(config::stream.file_apps, fileTree); } - } - catch(std::exception &e) { - BOOST_LOG(warning) << "SavePassword: "sv << e.what(); - outputTree.put("status", false); - outputTree.put("error", e.what()); - return; - } -} + catch (std::exception &e) { + BOOST_LOG(warning) << "SaveApp: "sv << e.what(); -void savePin(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; - - print_req(request); - - std::stringstream ss; - ss << request->content.rdbuf(); - - pt::ptree inputTree, outputTree; - - auto g = util::fail_guard([&]() { - std::ostringstream data; - pt::write_json(data, outputTree); - response->write(data.str()); - }); - - try { - // TODO: Input Validation - pt::read_json(ss, inputTree); - std::string pin = inputTree.get("pin"); - outputTree.put("status", nvhttp::pin(pin)); - } - catch(std::exception &e) { - BOOST_LOG(warning) << "SavePin: "sv << e.what(); - outputTree.put("status", false); - outputTree.put("error", e.what()); - return; - } -} - -void unpairAll(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; - - print_req(request); - - pt::ptree outputTree; - - auto g = util::fail_guard([&]() { - std::ostringstream data; - pt::write_json(data, outputTree); - response->write(data.str()); - }); - nvhttp::erase_all_clients(); - outputTree.put("status", true); -} - -void closeApp(resp_https_t response, req_https_t request) { - if(!authenticate(response, request)) return; - - print_req(request); - - pt::ptree outputTree; - - auto g = util::fail_guard([&]() { - std::ostringstream data; - pt::write_json(data, outputTree); - response->write(data.str()); - }); - - proc::proc.terminate(); - outputTree.put("status", true); -} - -void start() { - auto shutdown_event = mail::man->event(mail::shutdown); - - auto port_https = map_port(PORT_HTTPS); - - https_server_t server { config::nvhttp.cert, config::nvhttp.pkey }; - server.default_resource["GET"] = not_found; - server.resource["^/$"]["GET"] = getIndexPage; - server.resource["^/pin$"]["GET"] = getPinPage; - server.resource["^/apps$"]["GET"] = getAppsPage; - server.resource["^/clients$"]["GET"] = getClientsPage; - server.resource["^/config$"]["GET"] = getConfigPage; - server.resource["^/password$"]["GET"] = getPasswordPage; - server.resource["^/welcome$"]["GET"] = getWelcomePage; - server.resource["^/troubleshooting$"]["GET"] = getTroubleshootingPage; - server.resource["^/api/pin$"]["POST"] = savePin; - server.resource["^/api/apps$"]["GET"] = getApps; - server.resource["^/api/logs$"]["GET"] = getLogs; - server.resource["^/api/apps$"]["POST"] = saveApp; - server.resource["^/api/config$"]["GET"] = getConfig; - server.resource["^/api/config$"]["POST"] = saveConfig; - server.resource["^/api/restart$"]["POST"] = restart; - server.resource["^/api/password$"]["POST"] = savePassword; - server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; - server.resource["^/api/clients/unpair$"]["POST"] = unpairAll; - server.resource["^/api/apps/close$"]["POST"] = closeApp; - server.resource["^/api/covers/upload$"]["POST"] = uploadCover; - server.resource["^/images/favicon.ico$"]["GET"] = getFaviconImage; - server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage; - server.resource["^/node_modules\\/.+$"]["GET"] = getNodeModules; - server.config.reuse_address = true; - server.config.address = "0.0.0.0"s; - server.config.port = port_https; - - auto accept_and_run = [&](auto *server) { - try { - server->start([](unsigned short port) { - BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << port << "]"; - }); - } - catch(boost::system::system_error &err) { - // It's possible the exception gets thrown after calling server->stop() from a different thread - if(shutdown_event->peek()) { - return; - } - - BOOST_LOG(fatal) << "Couldn't start Configuration HTTPS server on port ["sv << port_https << "]: "sv << err.what(); - shutdown_event->raise(true); + outputTree.put("status", "false"); + outputTree.put("error", "Invalid Input JSON"); return; } - }; - std::thread tcp { accept_and_run, &server }; - // Wait for any event - shutdown_event->view(); + outputTree.put("status", "true"); + proc::refresh(config::stream.file_apps); + } - server.stop(); + void + deleteApp(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; - tcp.join(); -} -} // namespace confighttp + print_req(request); + + pt::ptree outputTree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_json(data, outputTree); + response->write(data.str()); + }); + pt::ptree fileTree; + try { + pt::read_json(config::stream.file_apps, fileTree); + auto &apps_node = fileTree.get_child("apps"s); + int index = stoi(request->path_match[1]); + + if (index < 0) { + outputTree.put("status", "false"); + outputTree.put("error", "Invalid Index"); + return; + } + else { + // Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick + pt::ptree newApps; + int i = 0; + for (const auto &kv : apps_node) { + if (i++ != index) { + newApps.push_back(std::make_pair("", kv.second)); + } + } + fileTree.erase("apps"); + fileTree.push_back(std::make_pair("apps", newApps)); + } + pt::write_json(config::stream.file_apps, fileTree); + } + catch (std::exception &e) { + BOOST_LOG(warning) << "DeleteApp: "sv << e.what(); + outputTree.put("status", "false"); + outputTree.put("error", "Invalid File JSON"); + return; + } + + outputTree.put("status", "true"); + proc::refresh(config::stream.file_apps); + } + + void + uploadCover(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + std::stringstream ss; + std::stringstream configStream; + ss << request->content.rdbuf(); + pt::ptree outputTree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + + SimpleWeb::StatusCode code = SimpleWeb::StatusCode::success_ok; + if (outputTree.get_child_optional("error").has_value()) { + code = SimpleWeb::StatusCode::client_error_bad_request; + } + + pt::write_json(data, outputTree); + response->write(code, data.str()); + }); + pt::ptree inputTree; + try { + pt::read_json(ss, inputTree); + } + catch (std::exception &e) { + BOOST_LOG(warning) << "UploadCover: "sv << e.what(); + outputTree.put("status", "false"); + outputTree.put("error", e.what()); + return; + } + + auto key = inputTree.get("key", ""); + if (key.empty()) { + outputTree.put("error", "Cover key is required"); + return; + } + auto url = inputTree.get("url", ""); + + const std::string coverdir = platf::appdata().string() + "/covers/"; + if (!boost::filesystem::exists(coverdir)) { + boost::filesystem::create_directory(coverdir); + } + + std::basic_string path = coverdir + http::url_escape(key) + ".png"; + if (!url.empty()) { + if (http::url_get_host(url) != "images.igdb.com") { + outputTree.put("error", "Only images.igdb.com is allowed"); + return; + } + if (!http::download_file(url, path)) { + outputTree.put("error", "Failed to download cover"); + return; + } + } + else { + auto data = SimpleWeb::Crypto::Base64::decode(inputTree.get("data")); + + std::ofstream imgfile(path); + imgfile.write(data.data(), (int) data.size()); + } + outputTree.put("path", path); + } + + void + getConfig(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + pt::ptree outputTree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + outputTree.put("status", "true"); + outputTree.put("platform", SUNSHINE_PLATFORM); + outputTree.put("version", PROJECT_VER); + outputTree.put("restart_supported", platf::restart_supported()); + + auto vars = config::parse_config(read_file(config::sunshine.config_file.c_str())); + + for (auto &[name, value] : vars) { + outputTree.put(std::move(name), std::move(value)); + } + } + + void + saveConfig(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + std::stringstream ss; + std::stringstream configStream; + ss << request->content.rdbuf(); + pt::ptree outputTree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_json(data, outputTree); + response->write(data.str()); + }); + pt::ptree inputTree; + try { + // TODO: Input Validation + pt::read_json(ss, inputTree); + for (const auto &kv : inputTree) { + std::string value = inputTree.get(kv.first); + if (value.length() == 0 || value.compare("null") == 0) continue; + + configStream << kv.first << " = " << value << std::endl; + } + write_file(config::sunshine.config_file.c_str(), configStream.str()); + } + catch (std::exception &e) { + BOOST_LOG(warning) << "SaveConfig: "sv << e.what(); + outputTree.put("status", "false"); + outputTree.put("error", e.what()); + return; + } + } + + void + restart(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + std::stringstream ss; + std::stringstream configStream; + ss << request->content.rdbuf(); + pt::ptree outputTree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + if (!platf::restart_supported()) { + outputTree.put("status", false); + outputTree.put("error", "Restart is not currently supported on this platform"); + return; + } + + if (!platf::restart()) { + outputTree.put("status", false); + outputTree.put("error", "Restart failed"); + return; + } + + outputTree.put("status", true); + } + + void + savePassword(resp_https_t response, req_https_t request) { + if (!config::sunshine.username.empty() && !authenticate(response, request)) return; + + print_req(request); + + std::stringstream ss; + std::stringstream configStream; + ss << request->content.rdbuf(); + + pt::ptree inputTree, outputTree; + + auto g = util::fail_guard([&]() { + std::ostringstream data; + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + try { + // TODO: Input Validation + pt::read_json(ss, inputTree); + auto username = inputTree.count("currentUsername") > 0 ? inputTree.get("currentUsername") : ""; + auto newUsername = inputTree.get("newUsername"); + auto password = inputTree.count("currentPassword") > 0 ? inputTree.get("currentPassword") : ""; + auto newPassword = inputTree.count("newPassword") > 0 ? inputTree.get("newPassword") : ""; + auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get("confirmNewPassword") : ""; + if (newUsername.length() == 0) newUsername = username; + if (newUsername.length() == 0) { + outputTree.put("status", false); + outputTree.put("error", "Invalid Username"); + } + else { + auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); + if (config::sunshine.username.empty() || (username == config::sunshine.username && hash == config::sunshine.password)) { + if (newPassword.empty() || newPassword != confirmPassword) { + outputTree.put("status", false); + outputTree.put("error", "Password Mismatch"); + } + else { + http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword); + http::reload_user_creds(config::sunshine.credentials_file); + outputTree.put("status", true); + } + } + else { + outputTree.put("status", false); + outputTree.put("error", "Invalid Current Credentials"); + } + } + } + catch (std::exception &e) { + BOOST_LOG(warning) << "SavePassword: "sv << e.what(); + outputTree.put("status", false); + outputTree.put("error", e.what()); + return; + } + } + + void + savePin(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + std::stringstream ss; + ss << request->content.rdbuf(); + + pt::ptree inputTree, outputTree; + + auto g = util::fail_guard([&]() { + std::ostringstream data; + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + try { + // TODO: Input Validation + pt::read_json(ss, inputTree); + std::string pin = inputTree.get("pin"); + outputTree.put("status", nvhttp::pin(pin)); + } + catch (std::exception &e) { + BOOST_LOG(warning) << "SavePin: "sv << e.what(); + outputTree.put("status", false); + outputTree.put("error", e.what()); + return; + } + } + + void + unpairAll(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + pt::ptree outputTree; + + auto g = util::fail_guard([&]() { + std::ostringstream data; + pt::write_json(data, outputTree); + response->write(data.str()); + }); + nvhttp::erase_all_clients(); + outputTree.put("status", true); + } + + void + closeApp(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + pt::ptree outputTree; + + auto g = util::fail_guard([&]() { + std::ostringstream data; + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + proc::proc.terminate(); + outputTree.put("status", true); + } + + void + start() { + auto shutdown_event = mail::man->event(mail::shutdown); + + auto port_https = map_port(PORT_HTTPS); + + https_server_t server { config::nvhttp.cert, config::nvhttp.pkey }; + server.default_resource["GET"] = not_found; + server.resource["^/$"]["GET"] = getIndexPage; + server.resource["^/pin$"]["GET"] = getPinPage; + server.resource["^/apps$"]["GET"] = getAppsPage; + server.resource["^/clients$"]["GET"] = getClientsPage; + server.resource["^/config$"]["GET"] = getConfigPage; + server.resource["^/password$"]["GET"] = getPasswordPage; + server.resource["^/welcome$"]["GET"] = getWelcomePage; + server.resource["^/troubleshooting$"]["GET"] = getTroubleshootingPage; + server.resource["^/api/pin$"]["POST"] = savePin; + server.resource["^/api/apps$"]["GET"] = getApps; + server.resource["^/api/logs$"]["GET"] = getLogs; + server.resource["^/api/apps$"]["POST"] = saveApp; + server.resource["^/api/config$"]["GET"] = getConfig; + server.resource["^/api/config$"]["POST"] = saveConfig; + server.resource["^/api/restart$"]["POST"] = restart; + server.resource["^/api/password$"]["POST"] = savePassword; + server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; + server.resource["^/api/clients/unpair$"]["POST"] = unpairAll; + server.resource["^/api/apps/close$"]["POST"] = closeApp; + server.resource["^/api/covers/upload$"]["POST"] = uploadCover; + server.resource["^/images/favicon.ico$"]["GET"] = getFaviconImage; + server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage; + server.resource["^/node_modules\\/.+$"]["GET"] = getNodeModules; + server.config.reuse_address = true; + server.config.address = "0.0.0.0"s; + server.config.port = port_https; + + auto accept_and_run = [&](auto *server) { + try { + server->start([](unsigned short port) { + BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << port << "]"; + }); + } + catch (boost::system::system_error &err) { + // It's possible the exception gets thrown after calling server->stop() from a different thread + if (shutdown_event->peek()) { + return; + } + + BOOST_LOG(fatal) << "Couldn't start Configuration HTTPS server on port ["sv << port_https << "]: "sv << err.what(); + shutdown_event->raise(true); + return; + } + }; + std::thread tcp { accept_and_run, &server }; + + // Wait for any event + shutdown_event->view(); + + server.stop(); + + tcp.join(); + } +} // namespace confighttp diff --git a/src/confighttp.h b/src/confighttp.h index b178ff1d..f5fe158a 100644 --- a/src/confighttp.h +++ b/src/confighttp.h @@ -10,11 +10,11 @@ #define WEB_DIR SUNSHINE_ASSETS_DIR "/web/" - namespace confighttp { -constexpr auto PORT_HTTPS = 1; -void start(); -} // namespace confighttp + constexpr auto PORT_HTTPS = 1; + void + start(); +} // namespace confighttp // mime types map const std::map mime_types = { @@ -35,4 +35,4 @@ const std::map mime_types = { { "xml", "text/xml" }, }; -#endif // SUNSHINE_CONFIGHTTP_H +#endif // SUNSHINE_CONFIGHTTP_H diff --git a/src/crypto.cpp b/src/crypto.cpp index bf994c83..f50c536c 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -4,484 +4,511 @@ #include namespace crypto { -using asn1_string_t = util::safe_ptr; + using asn1_string_t = util::safe_ptr; -cert_chain_t::cert_chain_t() : _certs {}, _cert_ctx { X509_STORE_CTX_new() } {} -void cert_chain_t::add(x509_t &&cert) { - x509_store_t x509_store { X509_STORE_new() }; + cert_chain_t::cert_chain_t(): + _certs {}, _cert_ctx { X509_STORE_CTX_new() } {} + void + cert_chain_t::add(x509_t &&cert) { + x509_store_t x509_store { X509_STORE_new() }; - X509_STORE_add_cert(x509_store.get(), cert.get()); - _certs.emplace_back(std::make_pair(std::move(cert), std::move(x509_store))); -} - -static int openssl_verify_cb(int ok, X509_STORE_CTX *ctx) { - int err_code = X509_STORE_CTX_get_error(ctx); - - switch(err_code) { - // Expired or not-yet-valid certificates are fine. Sometimes Moonlight is running on embedded devices - // that don't have accurate clocks (or haven't yet synchronized by the time Moonlight first runs). - // This behavior also matches what GeForce Experience does. - // FIXME: Checking for X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY is a temporary workaround to get moonlight-embedded to work on the raspberry pi - case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: - case X509_V_ERR_CERT_NOT_YET_VALID: - case X509_V_ERR_CERT_HAS_EXPIRED: - return 1; - - default: - return ok; + X509_STORE_add_cert(x509_store.get(), cert.get()); + _certs.emplace_back(std::make_pair(std::move(cert), std::move(x509_store))); } -} -/* + static int + openssl_verify_cb(int ok, X509_STORE_CTX *ctx) { + int err_code = X509_STORE_CTX_get_error(ctx); + + switch (err_code) { + // Expired or not-yet-valid certificates are fine. Sometimes Moonlight is running on embedded devices + // that don't have accurate clocks (or haven't yet synchronized by the time Moonlight first runs). + // This behavior also matches what GeForce Experience does. + // FIXME: Checking for X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY is a temporary workaround to get moonlight-embedded to work on the raspberry pi + case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: + case X509_V_ERR_CERT_NOT_YET_VALID: + case X509_V_ERR_CERT_HAS_EXPIRED: + return 1; + + default: + return ok; + } + } + + /* * When certificates from two or more instances of Moonlight have been added to x509_store_t, * only one of them will be verified by X509_verify_cert, resulting in only a single instance of * Moonlight to be able to use Sunshine * * To circumvent this, x509_store_t instance will be created for each instance of the certificates. */ -const char *cert_chain_t::verify(x509_t::element_type *cert) { - int err_code = 0; - for(auto &[_, x509_store] : _certs) { - auto fg = util::fail_guard([this]() { - X509_STORE_CTX_cleanup(_cert_ctx.get()); - }); + const char * + cert_chain_t::verify(x509_t::element_type *cert) { + int err_code = 0; + for (auto &[_, x509_store] : _certs) { + auto fg = util::fail_guard([this]() { + X509_STORE_CTX_cleanup(_cert_ctx.get()); + }); - X509_STORE_CTX_init(_cert_ctx.get(), x509_store.get(), cert, nullptr); - X509_STORE_CTX_set_verify_cb(_cert_ctx.get(), openssl_verify_cb); + X509_STORE_CTX_init(_cert_ctx.get(), x509_store.get(), cert, nullptr); + X509_STORE_CTX_set_verify_cb(_cert_ctx.get(), openssl_verify_cb); - // We don't care to validate the entire chain for the purposes of client auth. - // Some versions of clients forked from Moonlight Embedded produce client certs - // that OpenSSL doesn't detect as self-signed due to some X509v3 extensions. - X509_STORE_CTX_set_flags(_cert_ctx.get(), X509_V_FLAG_PARTIAL_CHAIN); + // We don't care to validate the entire chain for the purposes of client auth. + // Some versions of clients forked from Moonlight Embedded produce client certs + // that OpenSSL doesn't detect as self-signed due to some X509v3 extensions. + X509_STORE_CTX_set_flags(_cert_ctx.get(), X509_V_FLAG_PARTIAL_CHAIN); - auto err = X509_verify_cert(_cert_ctx.get()); + auto err = X509_verify_cert(_cert_ctx.get()); - if(err == 1) { - return nullptr; + if (err == 1) { + return nullptr; + } + + err_code = X509_STORE_CTX_get_error(_cert_ctx.get()); + + if (err_code != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && err_code != X509_V_ERR_INVALID_CA) { + return X509_verify_cert_error_string(err_code); + } } - err_code = X509_STORE_CTX_get_error(_cert_ctx.get()); + return X509_verify_cert_error_string(err_code); + } - if(err_code != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && err_code != X509_V_ERR_INVALID_CA) { - return X509_verify_cert_error_string(err_code); + namespace cipher { + + static int + init_decrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { + ctx.reset(EVP_CIPHER_CTX_new()); + + if (!ctx) { + return -1; + } + + if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) { + return -1; + } + + if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv->size(), nullptr) != 1) { + return -1; + } + + if (EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) { + return -1; + } + EVP_CIPHER_CTX_set_padding(ctx.get(), padding); + + return 0; } + + static int + init_encrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { + ctx.reset(EVP_CIPHER_CTX_new()); + + // Gen 7 servers use 128-bit AES ECB + if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) { + return -1; + } + + if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv->size(), nullptr) != 1) { + return -1; + } + + if (EVP_EncryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) { + return -1; + } + EVP_CIPHER_CTX_set_padding(ctx.get(), padding); + + return 0; + } + + static int + init_encrypt_cbc(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { + ctx.reset(EVP_CIPHER_CTX_new()); + + // Gen 7 servers use 128-bit AES ECB + if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_cbc(), nullptr, key->data(), iv->data()) != 1) { + return -1; + } + + EVP_CIPHER_CTX_set_padding(ctx.get(), padding); + + return 0; + } + + int + gcm_t::decrypt(const std::string_view &tagged_cipher, std::vector &plaintext, aes_t *iv) { + if (!decrypt_ctx && init_decrypt_gcm(decrypt_ctx, &key, iv, padding)) { + return -1; + } + + // Calling with cipher == nullptr results in a parameter change + // without requiring a reallocation of the internal cipher ctx. + if (EVP_DecryptInit_ex(decrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) { + return false; + } + + auto cipher = tagged_cipher.substr(tag_size); + auto tag = tagged_cipher.substr(0, tag_size); + + plaintext.resize((cipher.size() + 15) / 16 * 16); + + int size; + if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &size, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) { + return -1; + } + + if (EVP_CIPHER_CTX_ctrl(decrypt_ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast(tag.data())) != 1) { + return -1; + } + + int len = size; + if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + size, &len) != 1) { + return -1; + } + + plaintext.resize(size + len); + return 0; + } + + int + gcm_t::encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv) { + if (!encrypt_ctx && init_encrypt_gcm(encrypt_ctx, &key, iv, padding)) { + return -1; + } + + // Calling with cipher == nullptr results in a parameter change + // without requiring a reallocation of the internal cipher ctx. + if (EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) { + return -1; + } + + auto tag = tagged_cipher; + auto cipher = tag + tag_size; + + int len; + int size = round_to_pkcs7_padded(plaintext.size()); + + // Encrypt into the caller's buffer + if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &size, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { + return -1; + } + + // GCM encryption won't ever fill ciphertext here but we have to call it anyway + if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + size, &len) != 1) { + return -1; + } + + if (EVP_CIPHER_CTX_ctrl(encrypt_ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_size, tag) != 1) { + return -1; + } + + return len + size; + } + + int + ecb_t::decrypt(const std::string_view &cipher, std::vector &plaintext) { + int len; + + auto fg = util::fail_guard([this]() { + EVP_CIPHER_CTX_reset(decrypt_ctx.get()); + }); + + // Gen 7 servers use 128-bit AES ECB + if (EVP_DecryptInit_ex(decrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) { + return -1; + } + + EVP_CIPHER_CTX_set_padding(decrypt_ctx.get(), padding); + + plaintext.resize((cipher.size() + 15) / 16 * 16); + auto size = (int) plaintext.size(); + // Decrypt into the caller's buffer, leaving room for the auth tag to be prepended + if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &size, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) { + return -1; + } + + if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data(), &len) != 1) { + return -1; + } + + plaintext.resize(len + size); + return 0; + } + + int + ecb_t::encrypt(const std::string_view &plaintext, std::vector &cipher) { + auto fg = util::fail_guard([this]() { + EVP_CIPHER_CTX_reset(encrypt_ctx.get()); + }); + + // Gen 7 servers use 128-bit AES ECB + if (EVP_EncryptInit_ex(encrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) { + return -1; + } + + EVP_CIPHER_CTX_set_padding(encrypt_ctx.get(), padding); + + int len; + + cipher.resize((plaintext.size() + 15) / 16 * 16); + auto size = (int) cipher.size(); + + // Encrypt into the caller's buffer + if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher.data(), &size, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { + return -1; + } + + if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher.data() + size, &len) != 1) { + return -1; + } + + cipher.resize(len + size); + return 0; + } + + int + cbc_t::encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv) { + if (!encrypt_ctx && init_encrypt_cbc(encrypt_ctx, &key, iv, padding)) { + return -1; + } + + // Calling with cipher == nullptr results in a parameter change + // without requiring a reallocation of the internal cipher ctx. + if (EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) { + return false; + } + + int len; + + int size = plaintext.size(); // round_to_pkcs7_padded(plaintext.size()); + + // Encrypt into the caller's buffer + if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &size, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) { + return -1; + } + + if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + size, &len) != 1) { + return -1; + } + + return size + len; + } + + ecb_t::ecb_t(const aes_t &key, bool padding): + cipher_t { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_new(), key, padding } {} + + cbc_t::cbc_t(const aes_t &key, bool padding): + cipher_t { nullptr, nullptr, key, padding } {} + + gcm_t::gcm_t(const crypto::aes_t &key, bool padding): + cipher_t { nullptr, nullptr, key, padding } {} + + } // namespace cipher + + aes_t + gen_aes_key(const std::array &salt, const std::string_view &pin) { + aes_t key; + + std::string salt_pin; + salt_pin.reserve(salt.size() + pin.size()); + + salt_pin.insert(std::end(salt_pin), std::begin(salt), std::end(salt)); + salt_pin.insert(std::end(salt_pin), std::begin(pin), std::end(pin)); + + auto hsh = hash(salt_pin); + + std::copy(std::begin(hsh), std::begin(hsh) + key.size(), std::begin(key)); + + return key; } - return X509_verify_cert_error_string(err_code); -} - -namespace cipher { - -static int init_decrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { - ctx.reset(EVP_CIPHER_CTX_new()); - - if(!ctx) { - return -1; + sha256_t + hash(const std::string_view &plaintext) { + sha256_t hsh; + EVP_Digest(plaintext.data(), plaintext.size(), hsh.data(), nullptr, EVP_sha256(), nullptr); + return hsh; } - if(EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) { - return -1; + x509_t + x509(const std::string_view &x) { + bio_t io { BIO_new(BIO_s_mem()) }; + + BIO_write(io.get(), x.data(), x.size()); + + x509_t p; + PEM_read_bio_X509(io.get(), &p, nullptr, nullptr); + + return p; } - if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv->size(), nullptr) != 1) { - return -1; + pkey_t + pkey(const std::string_view &k) { + bio_t io { BIO_new(BIO_s_mem()) }; + + BIO_write(io.get(), k.data(), k.size()); + + pkey_t p = nullptr; + PEM_read_bio_PrivateKey(io.get(), &p, nullptr, nullptr); + + return p; } - if(EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) { - return -1; - } - EVP_CIPHER_CTX_set_padding(ctx.get(), padding); + std::string + pem(x509_t &x509) { + bio_t bio { BIO_new(BIO_s_mem()) }; - return 0; -} + PEM_write_bio_X509(bio.get(), x509.get()); + BUF_MEM *mem_ptr; + BIO_get_mem_ptr(bio.get(), &mem_ptr); -static int init_encrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { - ctx.reset(EVP_CIPHER_CTX_new()); - - // Gen 7 servers use 128-bit AES ECB - if(EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) { - return -1; + return { mem_ptr->data, mem_ptr->length }; } - if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv->size(), nullptr) != 1) { - return -1; + std::string + pem(pkey_t &pkey) { + bio_t bio { BIO_new(BIO_s_mem()) }; + + PEM_write_bio_PrivateKey(bio.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr); + BUF_MEM *mem_ptr; + BIO_get_mem_ptr(bio.get(), &mem_ptr); + + return { mem_ptr->data, mem_ptr->length }; } - if(EVP_EncryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) { - return -1; - } - EVP_CIPHER_CTX_set_padding(ctx.get(), padding); + std::string_view + signature(const x509_t &x) { + // X509_ALGOR *_ = nullptr; - return 0; -} + const ASN1_BIT_STRING *asn1 = nullptr; + X509_get0_signature(&asn1, nullptr, x.get()); -static int init_encrypt_cbc(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) { - ctx.reset(EVP_CIPHER_CTX_new()); - - // Gen 7 servers use 128-bit AES ECB - if(EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_cbc(), nullptr, key->data(), iv->data()) != 1) { - return -1; + return { (const char *) asn1->data, (std::size_t) asn1->length }; } - EVP_CIPHER_CTX_set_padding(ctx.get(), padding); + std::string + rand(std::size_t bytes) { + std::string r; + r.resize(bytes); - return 0; -} + RAND_bytes((uint8_t *) r.data(), r.size()); -int gcm_t::decrypt(const std::string_view &tagged_cipher, std::vector &plaintext, aes_t *iv) { - if(!decrypt_ctx && init_decrypt_gcm(decrypt_ctx, &key, iv, padding)) { - return -1; + return r; } - // Calling with cipher == nullptr results in a parameter change - // without requiring a reallocation of the internal cipher ctx. - if(EVP_DecryptInit_ex(decrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) { - return false; + std::vector + sign(const pkey_t &pkey, const std::string_view &data, const EVP_MD *md) { + md_ctx_t ctx { EVP_MD_CTX_create() }; + + if (EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, pkey.get()) != 1) { + return {}; + } + + if (EVP_DigestSignUpdate(ctx.get(), data.data(), data.size()) != 1) { + return {}; + } + + std::size_t slen = digest_size; + + std::vector digest; + digest.resize(slen); + + if (EVP_DigestSignFinal(ctx.get(), digest.data(), &slen) != 1) { + return {}; + } + + return digest; } - auto cipher = tagged_cipher.substr(tag_size); - auto tag = tagged_cipher.substr(0, tag_size); + creds_t + gen_creds(const std::string_view &cn, std::uint32_t key_bits) { + x509_t x509 { X509_new() }; + pkey_ctx_t ctx { EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr) }; + pkey_t pkey; - plaintext.resize((cipher.size() + 15) / 16 * 16); + EVP_PKEY_keygen_init(ctx.get()); + EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), key_bits); + EVP_PKEY_keygen(ctx.get(), &pkey); - int size; - if(EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) { - return -1; - } + X509_set_version(x509.get(), 2); - if(EVP_CIPHER_CTX_ctrl(decrypt_ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast(tag.data())) != 1) { - return -1; - } + // Generate a real serial number to avoid SEC_ERROR_REUSED_ISSUER_AND_SERIAL with Firefox + bignum_t serial { BN_new() }; + BN_rand(serial.get(), 159, BN_RAND_TOP_ANY, BN_RAND_BOTTOM_ANY); // 159 bits to fit in 20 bytes in DER format + BN_set_negative(serial.get(), 0); // Serial numbers must be positive + BN_to_ASN1_INTEGER(serial.get(), X509_get_serialNumber(x509.get())); - int len = size; - if(EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + size, &len) != 1) { - return -1; - } - - plaintext.resize(size + len); - return 0; -} - -int gcm_t::encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv) { - if(!encrypt_ctx && init_encrypt_gcm(encrypt_ctx, &key, iv, padding)) { - return -1; - } - - // Calling with cipher == nullptr results in a parameter change - // without requiring a reallocation of the internal cipher ctx. - if(EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) { - return -1; - } - - auto tag = tagged_cipher; - auto cipher = tag + tag_size; - - int len; - int size = round_to_pkcs7_padded(plaintext.size()); - - // Encrypt into the caller's buffer - if(EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &size, (const std::uint8_t *)plaintext.data(), plaintext.size()) != 1) { - return -1; - } - - // GCM encryption won't ever fill ciphertext here but we have to call it anyway - if(EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + size, &len) != 1) { - return -1; - } - - if(EVP_CIPHER_CTX_ctrl(encrypt_ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_size, tag) != 1) { - return -1; - } - - return len + size; -} - -int ecb_t::decrypt(const std::string_view &cipher, std::vector &plaintext) { - int len; - - auto fg = util::fail_guard([this]() { - EVP_CIPHER_CTX_reset(decrypt_ctx.get()); - }); - - // Gen 7 servers use 128-bit AES ECB - if(EVP_DecryptInit_ex(decrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) { - return -1; - } - - EVP_CIPHER_CTX_set_padding(decrypt_ctx.get(), padding); - - plaintext.resize((cipher.size() + 15) / 16 * 16); - auto size = (int)plaintext.size(); - // Decrypt into the caller's buffer, leaving room for the auth tag to be prepended - if(EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) { - return -1; - } - - if(EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data(), &len) != 1) { - return -1; - } - - plaintext.resize(len + size); - return 0; -} - -int ecb_t::encrypt(const std::string_view &plaintext, std::vector &cipher) { - auto fg = util::fail_guard([this]() { - EVP_CIPHER_CTX_reset(encrypt_ctx.get()); - }); - - // Gen 7 servers use 128-bit AES ECB - if(EVP_EncryptInit_ex(encrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) { - return -1; - } - - EVP_CIPHER_CTX_set_padding(encrypt_ctx.get(), padding); - - int len; - - cipher.resize((plaintext.size() + 15) / 16 * 16); - auto size = (int)cipher.size(); - - // Encrypt into the caller's buffer - if(EVP_EncryptUpdate(encrypt_ctx.get(), cipher.data(), &size, (const std::uint8_t *)plaintext.data(), plaintext.size()) != 1) { - return -1; - } - - if(EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher.data() + size, &len) != 1) { - return -1; - } - - cipher.resize(len + size); - return 0; -} - -int cbc_t::encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv) { - if(!encrypt_ctx && init_encrypt_cbc(encrypt_ctx, &key, iv, padding)) { - return -1; - } - - // Calling with cipher == nullptr results in a parameter change - // without requiring a reallocation of the internal cipher ctx. - if(EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) { - return false; - } - - int len; - - int size = plaintext.size(); // round_to_pkcs7_padded(plaintext.size()); - - // Encrypt into the caller's buffer - if(EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &size, (const std::uint8_t *)plaintext.data(), plaintext.size()) != 1) { - return -1; - } - - if(EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + size, &len) != 1) { - return -1; - } - - return size + len; -} - -ecb_t::ecb_t(const aes_t &key, bool padding) - : cipher_t { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_new(), key, padding } {} - -cbc_t::cbc_t(const aes_t &key, bool padding) - : cipher_t { nullptr, nullptr, key, padding } {} - -gcm_t::gcm_t(const crypto::aes_t &key, bool padding) - : cipher_t { nullptr, nullptr, key, padding } {} - -} // namespace cipher - -aes_t gen_aes_key(const std::array &salt, const std::string_view &pin) { - aes_t key; - - std::string salt_pin; - salt_pin.reserve(salt.size() + pin.size()); - - salt_pin.insert(std::end(salt_pin), std::begin(salt), std::end(salt)); - salt_pin.insert(std::end(salt_pin), std::begin(pin), std::end(pin)); - - auto hsh = hash(salt_pin); - - std::copy(std::begin(hsh), std::begin(hsh) + key.size(), std::begin(key)); - - return key; -} - -sha256_t hash(const std::string_view &plaintext) { - sha256_t hsh; - EVP_Digest(plaintext.data(), plaintext.size(), hsh.data(), nullptr, EVP_sha256(), nullptr); - return hsh; -} - -x509_t x509(const std::string_view &x) { - bio_t io { BIO_new(BIO_s_mem()) }; - - BIO_write(io.get(), x.data(), x.size()); - - x509_t p; - PEM_read_bio_X509(io.get(), &p, nullptr, nullptr); - - return p; -} - -pkey_t pkey(const std::string_view &k) { - bio_t io { BIO_new(BIO_s_mem()) }; - - BIO_write(io.get(), k.data(), k.size()); - - pkey_t p = nullptr; - PEM_read_bio_PrivateKey(io.get(), &p, nullptr, nullptr); - - return p; -} - -std::string pem(x509_t &x509) { - bio_t bio { BIO_new(BIO_s_mem()) }; - - PEM_write_bio_X509(bio.get(), x509.get()); - BUF_MEM *mem_ptr; - BIO_get_mem_ptr(bio.get(), &mem_ptr); - - return { mem_ptr->data, mem_ptr->length }; -} - -std::string pem(pkey_t &pkey) { - bio_t bio { BIO_new(BIO_s_mem()) }; - - PEM_write_bio_PrivateKey(bio.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr); - BUF_MEM *mem_ptr; - BIO_get_mem_ptr(bio.get(), &mem_ptr); - - return { mem_ptr->data, mem_ptr->length }; -} - -std::string_view signature(const x509_t &x) { - // X509_ALGOR *_ = nullptr; - - const ASN1_BIT_STRING *asn1 = nullptr; - X509_get0_signature(&asn1, nullptr, x.get()); - - return { (const char *)asn1->data, (std::size_t)asn1->length }; -} - -std::string rand(std::size_t bytes) { - std::string r; - r.resize(bytes); - - RAND_bytes((uint8_t *)r.data(), r.size()); - - return r; -} - -std::vector sign(const pkey_t &pkey, const std::string_view &data, const EVP_MD *md) { - md_ctx_t ctx { EVP_MD_CTX_create() }; - - if(EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, pkey.get()) != 1) { - return {}; - } - - if(EVP_DigestSignUpdate(ctx.get(), data.data(), data.size()) != 1) { - return {}; - } - - std::size_t slen = digest_size; - - std::vector digest; - digest.resize(slen); - - if(EVP_DigestSignFinal(ctx.get(), digest.data(), &slen) != 1) { - return {}; - } - - return digest; -} - -creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits) { - x509_t x509 { X509_new() }; - pkey_ctx_t ctx { EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr) }; - pkey_t pkey; - - EVP_PKEY_keygen_init(ctx.get()); - EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), key_bits); - EVP_PKEY_keygen(ctx.get(), &pkey); - - X509_set_version(x509.get(), 2); - - // Generate a real serial number to avoid SEC_ERROR_REUSED_ISSUER_AND_SERIAL with Firefox - bignum_t serial { BN_new() }; - BN_rand(serial.get(), 159, BN_RAND_TOP_ANY, BN_RAND_BOTTOM_ANY); // 159 bits to fit in 20 bytes in DER format - BN_set_negative(serial.get(), 0); // Serial numbers must be positive - BN_to_ASN1_INTEGER(serial.get(), X509_get_serialNumber(x509.get())); - - constexpr auto year = 60 * 60 * 24 * 365; + constexpr auto year = 60 * 60 * 24 * 365; #if OPENSSL_VERSION_NUMBER < 0x10100000L - X509_gmtime_adj(X509_get_notBefore(x509.get()), 0); - X509_gmtime_adj(X509_get_notAfter(x509.get()), 20 * year); + X509_gmtime_adj(X509_get_notBefore(x509.get()), 0); + X509_gmtime_adj(X509_get_notAfter(x509.get()), 20 * year); #else - asn1_string_t not_before { ASN1_STRING_dup(X509_get0_notBefore(x509.get())) }; - asn1_string_t not_after { ASN1_STRING_dup(X509_get0_notAfter(x509.get())) }; + asn1_string_t not_before { ASN1_STRING_dup(X509_get0_notBefore(x509.get())) }; + asn1_string_t not_after { ASN1_STRING_dup(X509_get0_notAfter(x509.get())) }; - X509_gmtime_adj(not_before.get(), 0); - X509_gmtime_adj(not_after.get(), 20 * year); + X509_gmtime_adj(not_before.get(), 0); + X509_gmtime_adj(not_after.get(), 20 * year); - X509_set1_notBefore(x509.get(), not_before.get()); - X509_set1_notAfter(x509.get(), not_after.get()); + X509_set1_notBefore(x509.get(), not_before.get()); + X509_set1_notAfter(x509.get(), not_after.get()); #endif - X509_set_pubkey(x509.get(), pkey.get()); + X509_set_pubkey(x509.get(), pkey.get()); - auto name = X509_get_subject_name(x509.get()); - X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, - (const std::uint8_t *)cn.data(), cn.size(), - -1, 0); + auto name = X509_get_subject_name(x509.get()); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, + (const std::uint8_t *) cn.data(), cn.size(), + -1, 0); - X509_set_issuer_name(x509.get(), name); - X509_sign(x509.get(), pkey.get(), EVP_sha256()); + X509_set_issuer_name(x509.get(), name); + X509_sign(x509.get(), pkey.get(), EVP_sha256()); - return { pem(x509), pem(pkey) }; -} - -std::vector sign256(const pkey_t &pkey, const std::string_view &data) { - return sign(pkey, data, EVP_sha256()); -} - -bool verify(const x509_t &x509, const std::string_view &data, const std::string_view &signature, const EVP_MD *md) { - auto pkey = X509_get_pubkey(x509.get()); - - md_ctx_t ctx { EVP_MD_CTX_create() }; - - if(EVP_DigestVerifyInit(ctx.get(), nullptr, md, nullptr, pkey) != 1) { - return false; + return { pem(x509), pem(pkey) }; } - if(EVP_DigestVerifyUpdate(ctx.get(), data.data(), data.size()) != 1) { - return false; + std::vector + sign256(const pkey_t &pkey, const std::string_view &data) { + return sign(pkey, data, EVP_sha256()); } - if(EVP_DigestVerifyFinal(ctx.get(), (const uint8_t *)signature.data(), signature.size()) != 1) { - return false; + bool + verify(const x509_t &x509, const std::string_view &data, const std::string_view &signature, const EVP_MD *md) { + auto pkey = X509_get_pubkey(x509.get()); + + md_ctx_t ctx { EVP_MD_CTX_create() }; + + if (EVP_DigestVerifyInit(ctx.get(), nullptr, md, nullptr, pkey) != 1) { + return false; + } + + if (EVP_DigestVerifyUpdate(ctx.get(), data.data(), data.size()) != 1) { + return false; + } + + if (EVP_DigestVerifyFinal(ctx.get(), (const uint8_t *) signature.data(), signature.size()) != 1) { + return false; + } + + return true; } - return true; -} - -bool verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature) { - return verify(x509, data, signature, EVP_sha256()); -} - -void md_ctx_destroy(EVP_MD_CTX *ctx) { - EVP_MD_CTX_destroy(ctx); -} - -std::string rand_alphabet(std::size_t bytes, const std::string_view &alphabet) { - auto value = rand(bytes); - - for(std::size_t i = 0; i != value.size(); ++i) { - value[i] = alphabet[value[i] % alphabet.length()]; + bool + verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature) { + return verify(x509, data, signature, EVP_sha256()); } - return value; -} -} // namespace crypto + void + md_ctx_destroy(EVP_MD_CTX *ctx) { + EVP_MD_CTX_destroy(ctx); + } + + std::string + rand_alphabet(std::size_t bytes, const std::string_view &alphabet) { + auto value = rand(bytes); + + for (std::size_t i = 0; i != value.size(); ++i) { + value[i] = alphabet[value[i] % alphabet.length()]; + } + return value; + } + +} // namespace crypto diff --git a/src/crypto.h b/src/crypto.h index 8a2547b1..2de5bafa 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -12,124 +12,148 @@ #include "utility.h" namespace crypto { -struct creds_t { - std::string x509; - std::string pkey; -}; -constexpr std::size_t digest_size = 256; + struct creds_t { + std::string x509; + std::string pkey; + }; + constexpr std::size_t digest_size = 256; -void md_ctx_destroy(EVP_MD_CTX *); + void + md_ctx_destroy(EVP_MD_CTX *); -using sha256_t = std::array; + using sha256_t = std::array; -using aes_t = std::array; -using x509_t = util::safe_ptr; -using x509_store_t = util::safe_ptr; -using x509_store_ctx_t = util::safe_ptr; -using cipher_ctx_t = util::safe_ptr; -using md_ctx_t = util::safe_ptr; -using bio_t = util::safe_ptr; -using pkey_t = util::safe_ptr; -using pkey_ctx_t = util::safe_ptr; -using bignum_t = util::safe_ptr; + using aes_t = std::array; + using x509_t = util::safe_ptr; + using x509_store_t = util::safe_ptr; + using x509_store_ctx_t = util::safe_ptr; + using cipher_ctx_t = util::safe_ptr; + using md_ctx_t = util::safe_ptr; + using bio_t = util::safe_ptr; + using pkey_t = util::safe_ptr; + using pkey_ctx_t = util::safe_ptr; + using bignum_t = util::safe_ptr; -sha256_t hash(const std::string_view &plaintext); + sha256_t + hash(const std::string_view &plaintext); -aes_t gen_aes_key(const std::array &salt, const std::string_view &pin); + aes_t + gen_aes_key(const std::array &salt, const std::string_view &pin); -x509_t x509(const std::string_view &x); -pkey_t pkey(const std::string_view &k); -std::string pem(x509_t &x509); -std::string pem(pkey_t &pkey); + x509_t + x509(const std::string_view &x); + pkey_t + pkey(const std::string_view &k); + std::string + pem(x509_t &x509); + std::string + pem(pkey_t &pkey); -std::vector sign256(const pkey_t &pkey, const std::string_view &data); -bool verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature); + std::vector + sign256(const pkey_t &pkey, const std::string_view &data); + bool + verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature); -creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits); + creds_t + gen_creds(const std::string_view &cn, std::uint32_t key_bits); -std::string_view signature(const x509_t &x); + std::string_view + signature(const x509_t &x); -std::string rand(std::size_t bytes); -std::string rand_alphabet(std::size_t bytes, - const std::string_view &alphabet = std::string_view { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!%&()=-" }); + std::string + rand(std::size_t bytes); + std::string + rand_alphabet(std::size_t bytes, + const std::string_view &alphabet = std::string_view { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!%&()=-" }); -class cert_chain_t { -public: - KITTY_DECL_CONSTR(cert_chain_t) + class cert_chain_t { + public: + KITTY_DECL_CONSTR(cert_chain_t) - void add(x509_t &&cert); + void + add(x509_t &&cert); - const char *verify(x509_t::element_type *cert); + const char * + verify(x509_t::element_type *cert); -private: - std::vector> _certs; - x509_store_ctx_t _cert_ctx; -}; + private: + std::vector> _certs; + x509_store_ctx_t _cert_ctx; + }; -namespace cipher { -constexpr std::size_t tag_size = 16; -constexpr std::size_t round_to_pkcs7_padded(std::size_t size) { - return ((size + 15) / 16) * 16; -} + namespace cipher { + constexpr std::size_t tag_size = 16; + constexpr std::size_t + round_to_pkcs7_padded(std::size_t size) { + return ((size + 15) / 16) * 16; + } -class cipher_t { -public: - cipher_ctx_t decrypt_ctx; - cipher_ctx_t encrypt_ctx; + class cipher_t { + public: + cipher_ctx_t decrypt_ctx; + cipher_ctx_t encrypt_ctx; - aes_t key; + aes_t key; - bool padding; -}; + bool padding; + }; -class ecb_t : public cipher_t { -public: - ecb_t() = default; - ecb_t(ecb_t &&) noexcept = default; - ecb_t &operator=(ecb_t &&) noexcept = default; + class ecb_t: public cipher_t { + public: + ecb_t() = default; + ecb_t(ecb_t &&) noexcept = default; + ecb_t & + operator=(ecb_t &&) noexcept = default; - ecb_t(const aes_t &key, bool padding = true); + ecb_t(const aes_t &key, bool padding = true); - int encrypt(const std::string_view &plaintext, std::vector &cipher); - int decrypt(const std::string_view &cipher, std::vector &plaintext); -}; + int + encrypt(const std::string_view &plaintext, std::vector &cipher); + int + decrypt(const std::string_view &cipher, std::vector &plaintext); + }; -class gcm_t : public cipher_t { -public: - gcm_t() = default; - gcm_t(gcm_t &&) noexcept = default; - gcm_t &operator=(gcm_t &&) noexcept = default; + class gcm_t: public cipher_t { + public: + gcm_t() = default; + gcm_t(gcm_t &&) noexcept = default; + gcm_t & + operator=(gcm_t &&) noexcept = default; - gcm_t(const crypto::aes_t &key, bool padding = true); + gcm_t(const crypto::aes_t &key, bool padding = true); - /** + /** * length of cipher must be at least: round_to_pkcs7_padded(plaintext.size()) + crypto::cipher::tag_size * * return -1 on error * return bytes written on success */ - int encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv); + int + encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv); - int decrypt(const std::string_view &cipher, std::vector &plaintext, aes_t *iv); -}; + int + decrypt(const std::string_view &cipher, std::vector &plaintext, aes_t *iv); + }; -class cbc_t : public cipher_t { -public: - cbc_t() = default; - cbc_t(cbc_t &&) noexcept = default; - cbc_t &operator=(cbc_t &&) noexcept = default; + class cbc_t: public cipher_t { + public: + cbc_t() = default; + cbc_t(cbc_t &&) noexcept = default; + cbc_t & + operator=(cbc_t &&) noexcept = default; - cbc_t(const crypto::aes_t &key, bool padding = true); + cbc_t(const crypto::aes_t &key, bool padding = true); - /** + /** * length of cipher must be at least: round_to_pkcs7_padded(plaintext.size()) * * return -1 on error * return bytes written on success */ - int encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv); -}; -} // namespace cipher -} // namespace crypto + int + encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv); + }; + } // namespace cipher +} // namespace crypto -#endif //SUNSHINE_CRYPTO_H +#endif //SUNSHINE_CRYPTO_H diff --git a/src/httpcommon.cpp b/src/httpcommon.cpp index 95b9f641..24810f51 100644 --- a/src/httpcommon.cpp +++ b/src/httpcommon.cpp @@ -27,209 +27,219 @@ #include "uuid.h" namespace http { -using namespace std::literals; -namespace fs = std::filesystem; -namespace pt = boost::property_tree; + using namespace std::literals; + namespace fs = std::filesystem; + namespace pt = boost::property_tree; -int reload_user_creds(const std::string &file); -bool user_creds_exist(const std::string &file); + int + reload_user_creds(const std::string &file); + bool + user_creds_exist(const std::string &file); -std::string unique_id; -net::net_e origin_pin_allowed; -net::net_e origin_web_ui_allowed; + std::string unique_id; + net::net_e origin_pin_allowed; + net::net_e origin_web_ui_allowed; -int init() { - bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE]; - origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed); - origin_web_ui_allowed = net::from_enum_string(config::nvhttp.origin_web_ui_allowed); + int + init() { + bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE]; + origin_pin_allowed = net::from_enum_string(config::nvhttp.origin_pin_allowed); + origin_web_ui_allowed = net::from_enum_string(config::nvhttp.origin_web_ui_allowed); - if(clean_slate) { - unique_id = uuid_util::uuid_t::generate().string(); - auto dir = std::filesystem::temp_directory_path() / "Sunshine"sv; - config::nvhttp.cert = (dir / ("cert-"s + unique_id)).string(); - config::nvhttp.pkey = (dir / ("pkey-"s + unique_id)).string(); - } - - if(!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) { - if(create_creds(config::nvhttp.pkey, config::nvhttp.cert)) { - return -1; + if (clean_slate) { + unique_id = uuid_util::uuid_t::generate().string(); + auto dir = std::filesystem::temp_directory_path() / "Sunshine"sv; + config::nvhttp.cert = (dir / ("cert-"s + unique_id)).string(); + config::nvhttp.pkey = (dir / ("pkey-"s + unique_id)).string(); } - } - if(user_creds_exist(config::sunshine.credentials_file)) { - if(reload_user_creds(config::sunshine.credentials_file)) return -1; - } - else { - BOOST_LOG(info) << "Open the Web UI to set your new username and password and getting started"; - } - return 0; -} -int save_user_creds(const std::string &file, const std::string &username, const std::string &password, bool run_our_mouth) { - pt::ptree outputTree; + if (!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) { + if (create_creds(config::nvhttp.pkey, config::nvhttp.cert)) { + return -1; + } + } + if (user_creds_exist(config::sunshine.credentials_file)) { + if (reload_user_creds(config::sunshine.credentials_file)) return -1; + } + else { + BOOST_LOG(info) << "Open the Web UI to set your new username and password and getting started"; + } + return 0; + } - if(fs::exists(file)) { + int + save_user_creds(const std::string &file, const std::string &username, const std::string &password, bool run_our_mouth) { + pt::ptree outputTree; + + if (fs::exists(file)) { + try { + pt::read_json(file, outputTree); + } + catch (std::exception &e) { + BOOST_LOG(error) << "Couldn't read user credentials: "sv << e.what(); + return -1; + } + } + + auto salt = crypto::rand_alphabet(16); + outputTree.put("username", username); + outputTree.put("salt", salt); + outputTree.put("password", util::hex(crypto::hash(password + salt)).to_string()); try { - pt::read_json(file, outputTree); + pt::write_json(file, outputTree); } - catch(std::exception &e) { - BOOST_LOG(error) << "Couldn't read user credentials: "sv << e.what(); + catch (std::exception &e) { + BOOST_LOG(error) << "generating user credentials: "sv << e.what(); return -1; } + + BOOST_LOG(info) << "New credentials have been created"sv; + return 0; } - auto salt = crypto::rand_alphabet(16); - outputTree.put("username", username); - outputTree.put("salt", salt); - outputTree.put("password", util::hex(crypto::hash(password + salt)).to_string()); - try { - pt::write_json(file, outputTree); - } - catch(std::exception &e) { - BOOST_LOG(error) << "generating user credentials: "sv << e.what(); - return -1; - } + bool + user_creds_exist(const std::string &file) { + if (!fs::exists(file)) { + return false; + } - BOOST_LOG(info) << "New credentials have been created"sv; - return 0; -} + pt::ptree inputTree; + try { + pt::read_json(file, inputTree); + return inputTree.find("username") != inputTree.not_found() && + inputTree.find("password") != inputTree.not_found() && + inputTree.find("salt") != inputTree.not_found(); + } + catch (std::exception &e) { + BOOST_LOG(error) << "validating user credentials: "sv << e.what(); + } -bool user_creds_exist(const std::string &file) { - if(!fs::exists(file)) { return false; } - pt::ptree inputTree; - try { - pt::read_json(file, inputTree); - return inputTree.find("username") != inputTree.not_found() && - inputTree.find("password") != inputTree.not_found() && - inputTree.find("salt") != inputTree.not_found(); - } - catch(std::exception &e) { - BOOST_LOG(error) << "validating user credentials: "sv << e.what(); + int + reload_user_creds(const std::string &file) { + pt::ptree inputTree; + try { + pt::read_json(file, inputTree); + config::sunshine.username = inputTree.get("username"); + config::sunshine.password = inputTree.get("password"); + config::sunshine.salt = inputTree.get("salt"); + } + catch (std::exception &e) { + BOOST_LOG(error) << "loading user credentials: "sv << e.what(); + return -1; + } + return 0; } - return false; -} + int + create_creds(const std::string &pkey, const std::string &cert) { + fs::path pkey_path = pkey; + fs::path cert_path = cert; -int reload_user_creds(const std::string &file) { - pt::ptree inputTree; - try { - pt::read_json(file, inputTree); - config::sunshine.username = inputTree.get("username"); - config::sunshine.password = inputTree.get("password"); - config::sunshine.salt = inputTree.get("salt"); - } - catch(std::exception &e) { - BOOST_LOG(error) << "loading user credentials: "sv << e.what(); - return -1; - } - return 0; -} + auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048); -int create_creds(const std::string &pkey, const std::string &cert) { - fs::path pkey_path = pkey; - fs::path cert_path = cert; + auto pkey_dir = pkey_path; + auto cert_dir = cert_path; + pkey_dir.remove_filename(); + cert_dir.remove_filename(); - auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048); + std::error_code err_code {}; + fs::create_directories(pkey_dir, err_code); + if (err_code) { + BOOST_LOG(error) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message(); + return -1; + } - auto pkey_dir = pkey_path; - auto cert_dir = cert_path; - pkey_dir.remove_filename(); - cert_dir.remove_filename(); + fs::create_directories(cert_dir, err_code); + if (err_code) { + BOOST_LOG(error) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message(); + return -1; + } - std::error_code err_code {}; - fs::create_directories(pkey_dir, err_code); - if(err_code) { - BOOST_LOG(error) << "Couldn't create directory ["sv << pkey_dir << "] :"sv << err_code.message(); - return -1; + if (write_file(pkey.c_str(), creds.pkey)) { + BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.pkey << ']'; + return -1; + } + + if (write_file(cert.c_str(), creds.x509)) { + BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.cert << ']'; + return -1; + } + + fs::permissions(pkey_path, + fs::perms::owner_read | fs::perms::owner_write, + fs::perm_options::replace, err_code); + + if (err_code) { + BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message(); + return -1; + } + + fs::permissions(cert_path, + fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write, + fs::perm_options::replace, err_code); + + if (err_code) { + BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message(); + return -1; + } + + return 0; } - fs::create_directories(cert_dir, err_code); - if(err_code) { - BOOST_LOG(error) << "Couldn't create directory ["sv << cert_dir << "] :"sv << err_code.message(); - return -1; - } - - if(write_file(pkey.c_str(), creds.pkey)) { - BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.pkey << ']'; - return -1; - } - - if(write_file(cert.c_str(), creds.x509)) { - BOOST_LOG(error) << "Couldn't open ["sv << config::nvhttp.cert << ']'; - return -1; - } - - fs::permissions(pkey_path, - fs::perms::owner_read | fs::perms::owner_write, - fs::perm_options::replace, err_code); - - if(err_code) { - BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.pkey << "] :"sv << err_code.message(); - return -1; - } - - fs::permissions(cert_path, - fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write, - fs::perm_options::replace, err_code); - - if(err_code) { - BOOST_LOG(error) << "Couldn't change permissions of ["sv << config::nvhttp.cert << "] :"sv << err_code.message(); - return -1; - } - - return 0; -} - -bool download_file(const std::string &url, const std::string &file) { - CURL *curl = curl_easy_init(); - if(!curl) { - BOOST_LOG(error) << "Couldn't create CURL instance"; - return false; - } - FILE *fp = fopen(file.c_str(), "wb"); - if(!fp) { - BOOST_LOG(error) << "Couldn't open ["sv << file << ']'; - curl_easy_cleanup(curl); - return false; - } - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); + bool + download_file(const std::string &url, const std::string &file) { + CURL *curl = curl_easy_init(); + if (!curl) { + BOOST_LOG(error) << "Couldn't create CURL instance"; + return false; + } + FILE *fp = fopen(file.c_str(), "wb"); + if (!fp) { + BOOST_LOG(error) << "Couldn't open ["sv << file << ']'; + curl_easy_cleanup(curl); + return false; + } + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); #ifdef _WIN32 - curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); #endif - CURLcode result = curl_easy_perform(curl); - if(result != CURLE_OK) { - BOOST_LOG(error) << "Couldn't download ["sv << url << ", code:" << result << ']'; + CURLcode result = curl_easy_perform(curl); + if (result != CURLE_OK) { + BOOST_LOG(error) << "Couldn't download ["sv << url << ", code:" << result << ']'; + } + curl_easy_cleanup(curl); + fclose(fp); + return result == CURLE_OK; } - curl_easy_cleanup(curl); - fclose(fp); - return result == CURLE_OK; -} -std::string url_escape(const std::string &url) { - CURL *curl = curl_easy_init(); - char *string = curl_easy_escape(curl, url.c_str(), url.length()); - std::string result(string); - curl_free(string); - curl_easy_cleanup(curl); - return result; -} + std::string + url_escape(const std::string &url) { + CURL *curl = curl_easy_init(); + char *string = curl_easy_escape(curl, url.c_str(), url.length()); + std::string result(string); + curl_free(string); + curl_easy_cleanup(curl); + return result; + } -std::string url_get_host(const std::string &url) { - CURLU *curlu = curl_url(); - curl_url_set(curlu, CURLUPART_URL, url.c_str(), url.length()); - char *host; - if(curl_url_get(curlu, CURLUPART_HOST, &host, 0) != CURLUE_OK) { + std::string + url_get_host(const std::string &url) { + CURLU *curlu = curl_url(); + curl_url_set(curlu, CURLUPART_URL, url.c_str(), url.length()); + char *host; + if (curl_url_get(curlu, CURLUPART_HOST, &host, 0) != CURLUE_OK) { + curl_url_cleanup(curlu); + return ""; + } + std::string result(host); + curl_free(host); curl_url_cleanup(curlu); - return ""; + return result; } - std::string result(host); - curl_free(host); - curl_url_cleanup(curlu); - return result; -} -} // namespace http +} // namespace http diff --git a/src/httpcommon.h b/src/httpcommon.h index 4e8eee01..4deecd14 100644 --- a/src/httpcommon.h +++ b/src/httpcommon.h @@ -3,21 +3,28 @@ namespace http { -int init(); -int create_creds(const std::string &pkey, const std::string &cert); -int save_user_creds( - const std::string &file, - const std::string &username, - const std::string &password, - bool run_our_mouth = false); + int + init(); + int + create_creds(const std::string &pkey, const std::string &cert); + int + save_user_creds( + const std::string &file, + const std::string &username, + const std::string &password, + bool run_our_mouth = false); -int reload_user_creds(const std::string &file); -bool download_file(const std::string &url, const std::string &file); -std::string url_escape(const std::string &url); -std::string url_get_host(const std::string &url); + int + reload_user_creds(const std::string &file); + bool + download_file(const std::string &url, const std::string &file); + std::string + url_escape(const std::string &url); + std::string + url_get_host(const std::string &url); -extern std::string unique_id; -extern net::net_e origin_pin_allowed; -extern net::net_e origin_web_ui_allowed; + extern std::string unique_id; + extern net::net_e origin_pin_allowed; + extern net::net_e origin_web_ui_allowed; -} // namespace http +} // namespace http diff --git a/src/input.cpp b/src/input.cpp index 54d887e7..b0fc2791 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -20,93 +20,97 @@ extern "C" { using namespace std::literals; namespace input { -constexpr auto MAX_GAMEPADS = std::min((std::size_t)platf::MAX_GAMEPADS, sizeof(std::int16_t) * 8); -#define DISABLE_LEFT_BUTTON_DELAY ((thread_pool_util::ThreadPool::task_id_t)0x01) + constexpr auto MAX_GAMEPADS = std::min((std::size_t) platf::MAX_GAMEPADS, sizeof(std::int16_t) * 8); +#define DISABLE_LEFT_BUTTON_DELAY ((thread_pool_util::ThreadPool::task_id_t) 0x01) #define ENABLE_LEFT_BUTTON_DELAY nullptr -constexpr auto VKEY_SHIFT = 0x10; -constexpr auto VKEY_LSHIFT = 0xA0; -constexpr auto VKEY_RSHIFT = 0xA1; -constexpr auto VKEY_CONTROL = 0x11; -constexpr auto VKEY_LCONTROL = 0xA2; -constexpr auto VKEY_RCONTROL = 0xA3; -constexpr auto VKEY_MENU = 0x12; -constexpr auto VKEY_LMENU = 0xA4; -constexpr auto VKEY_RMENU = 0xA5; + constexpr auto VKEY_SHIFT = 0x10; + constexpr auto VKEY_LSHIFT = 0xA0; + constexpr auto VKEY_RSHIFT = 0xA1; + constexpr auto VKEY_CONTROL = 0x11; + constexpr auto VKEY_LCONTROL = 0xA2; + constexpr auto VKEY_RCONTROL = 0xA3; + constexpr auto VKEY_MENU = 0x12; + constexpr auto VKEY_LMENU = 0xA4; + constexpr auto VKEY_RMENU = 0xA5; -enum class button_state_e { - NONE, - DOWN, - UP -}; - -template -int alloc_id(std::bitset &gamepad_mask) { - for(int x = 0; x < gamepad_mask.size(); ++x) { - if(!gamepad_mask[x]) { - gamepad_mask[x] = true; - return x; - } - } - - return -1; -} - -template -void free_id(std::bitset &gamepad_mask, int id) { - gamepad_mask[id] = false; -} - -static task_pool_util::TaskPool::task_id_t key_press_repeat_id {}; -static std::unordered_map key_press {}; -static std::array mouse_press {}; - -static platf::input_t platf_input; -static std::bitset gamepadMask {}; - -void free_gamepad(platf::input_t &platf_input, int id) { - platf::gamepad(platf_input, id, platf::gamepad_state_t {}); - platf::free_gamepad(platf_input, id); - - free_id(gamepadMask, id); -} -struct gamepad_t { - gamepad_t() : gamepad_state {}, back_timeout_id {}, id { -1 }, back_button_state { button_state_e::NONE } {} - ~gamepad_t() { - if(id >= 0) { - task_pool.push([id = this->id]() { - free_gamepad(platf_input, id); - }); - } - } - - platf::gamepad_state_t gamepad_state; - - thread_pool_util::ThreadPool::task_id_t back_timeout_id; - - int id; - - // When emulating the HOME button, we may need to artificially release the back button. - // Afterwards, the gamepad state on sunshine won't match the state on Moonlight. - // To prevent Sunshine from sending erroneous input data to the active application, - // Sunshine forces the button to be in a specific state until the gamepad state matches that of - // Moonlight once more. - button_state_e back_button_state; -}; - -struct input_t { - enum shortkey_e { - CTRL = 0x1, - ALT = 0x2, - SHIFT = 0x4, - - SHORTCUT = CTRL | ALT | SHIFT + enum class button_state_e { + NONE, + DOWN, + UP }; - input_t( - safe::mail_raw_t::event_t touch_port_event, - platf::rumble_queue_t rumble_queue) - : shortcutFlags {}, + template + int + alloc_id(std::bitset &gamepad_mask) { + for (int x = 0; x < gamepad_mask.size(); ++x) { + if (!gamepad_mask[x]) { + gamepad_mask[x] = true; + return x; + } + } + + return -1; + } + + template + void + free_id(std::bitset &gamepad_mask, int id) { + gamepad_mask[id] = false; + } + + static task_pool_util::TaskPool::task_id_t key_press_repeat_id {}; + static std::unordered_map key_press {}; + static std::array mouse_press {}; + + static platf::input_t platf_input; + static std::bitset gamepadMask {}; + + void + free_gamepad(platf::input_t &platf_input, int id) { + platf::gamepad(platf_input, id, platf::gamepad_state_t {}); + platf::free_gamepad(platf_input, id); + + free_id(gamepadMask, id); + } + struct gamepad_t { + gamepad_t(): + gamepad_state {}, back_timeout_id {}, id { -1 }, back_button_state { button_state_e::NONE } {} + ~gamepad_t() { + if (id >= 0) { + task_pool.push([id = this->id]() { + free_gamepad(platf_input, id); + }); + } + } + + platf::gamepad_state_t gamepad_state; + + thread_pool_util::ThreadPool::task_id_t back_timeout_id; + + int id; + + // When emulating the HOME button, we may need to artificially release the back button. + // Afterwards, the gamepad state on sunshine won't match the state on Moonlight. + // To prevent Sunshine from sending erroneous input data to the active application, + // Sunshine forces the button to be in a specific state until the gamepad state matches that of + // Moonlight once more. + button_state_e back_button_state; + }; + + struct input_t { + enum shortkey_e { + CTRL = 0x1, + ALT = 0x2, + SHIFT = 0x4, + + SHORTCUT = CTRL | ALT | SHIFT + }; + + input_t( + safe::mail_raw_t::event_t touch_port_event, + platf::rumble_queue_t rumble_queue): + shortcutFlags {}, active_gamepad_state {}, gamepads(MAX_GAMEPADS), touch_port_event { std::move(touch_port_event) }, @@ -114,229 +118,242 @@ struct input_t { mouse_left_button_timeout {}, touch_port { 0, 0, 0, 0, 0, 0, 1.0f } {} - // Keep track of alt+ctrl+shift key combo - int shortcutFlags; + // Keep track of alt+ctrl+shift key combo + int shortcutFlags; - std::uint16_t active_gamepad_state; - std::vector gamepads; + std::uint16_t active_gamepad_state; + std::vector gamepads; - safe::mail_raw_t::event_t touch_port_event; - platf::rumble_queue_t rumble_queue; + safe::mail_raw_t::event_t touch_port_event; + platf::rumble_queue_t rumble_queue; - thread_pool_util::ThreadPool::task_id_t mouse_left_button_timeout; + thread_pool_util::ThreadPool::task_id_t mouse_left_button_timeout; - input::touch_port_t touch_port; -}; + input::touch_port_t touch_port; + }; -/** + /** * Apply shortcut based on VKEY * On success * return > 0 * On nothing * return 0 */ -inline int apply_shortcut(short keyCode) { - constexpr auto VK_F1 = 0x70; - constexpr auto VK_F13 = 0x7C; + inline int + apply_shortcut(short keyCode) { + constexpr auto VK_F1 = 0x70; + constexpr auto VK_F13 = 0x7C; - BOOST_LOG(debug) << "Apply Shortcut: 0x"sv << util::hex((std::uint8_t)keyCode).to_string_view(); + BOOST_LOG(debug) << "Apply Shortcut: 0x"sv << util::hex((std::uint8_t) keyCode).to_string_view(); - if(keyCode >= VK_F1 && keyCode <= VK_F13) { - mail::man->event(mail::switch_display)->raise(keyCode - VK_F1); - return 1; + if (keyCode >= VK_F1 && keyCode <= VK_F13) { + mail::man->event(mail::switch_display)->raise(keyCode - VK_F1); + return 1; + } + + switch (keyCode) { + case 0x4E /* VKEY_N */: + display_cursor = !display_cursor; + return 1; + } + + return 0; } - switch(keyCode) { - case 0x4E /* VKEY_N */: - display_cursor = !display_cursor; - return 1; + void + print(PNV_REL_MOUSE_MOVE_PACKET packet) { + BOOST_LOG(debug) + << "--begin relative mouse move packet--"sv << std::endl + << "deltaX ["sv << util::endian::big(packet->deltaX) << ']' << std::endl + << "deltaY ["sv << util::endian::big(packet->deltaY) << ']' << std::endl + << "--end relative mouse move packet--"sv; } - return 0; -} - -void print(PNV_REL_MOUSE_MOVE_PACKET packet) { - BOOST_LOG(debug) - << "--begin relative mouse move packet--"sv << std::endl - << "deltaX ["sv << util::endian::big(packet->deltaX) << ']' << std::endl - << "deltaY ["sv << util::endian::big(packet->deltaY) << ']' << std::endl - << "--end relative mouse move packet--"sv; -} - -void print(PNV_ABS_MOUSE_MOVE_PACKET packet) { - BOOST_LOG(debug) - << "--begin absolute mouse move packet--"sv << std::endl - << "x ["sv << util::endian::big(packet->x) << ']' << std::endl - << "y ["sv << util::endian::big(packet->y) << ']' << std::endl - << "width ["sv << util::endian::big(packet->width) << ']' << std::endl - << "height ["sv << util::endian::big(packet->height) << ']' << std::endl - << "--end absolute mouse move packet--"sv; -} - -void print(PNV_MOUSE_BUTTON_PACKET packet) { - BOOST_LOG(debug) - << "--begin mouse button packet--"sv << std::endl - << "action ["sv << util::hex(packet->header.magic).to_string_view() << ']' << std::endl - << "button ["sv << util::hex(packet->button).to_string_view() << ']' << std::endl - << "--end mouse button packet--"sv; -} - -void print(PNV_SCROLL_PACKET packet) { - BOOST_LOG(debug) - << "--begin mouse scroll packet--"sv << std::endl - << "scrollAmt1 ["sv << util::endian::big(packet->scrollAmt1) << ']' << std::endl - << "--end mouse scroll packet--"sv; -} - -void print(PSS_HSCROLL_PACKET packet) { - BOOST_LOG(debug) - << "--begin mouse hscroll packet--"sv << std::endl - << "scrollAmount ["sv << util::endian::big(packet->scrollAmount) << ']' << std::endl - << "--end mouse hscroll packet--"sv; -} - -void print(PNV_KEYBOARD_PACKET packet) { - BOOST_LOG(debug) - << "--begin keyboard packet--"sv << std::endl - << "keyAction ["sv << util::hex(packet->header.magic).to_string_view() << ']' << std::endl - << "keyCode ["sv << util::hex(packet->keyCode).to_string_view() << ']' << std::endl - << "modifiers ["sv << util::hex(packet->modifiers).to_string_view() << ']' << std::endl - << "flags ["sv << util::hex(packet->flags).to_string_view() << ']' << std::endl - << "--end keyboard packet--"sv; -} - -void print(PNV_UNICODE_PACKET packet) { - std::string text(packet->text, util::endian::big(packet->header.size) - sizeof(packet->header.magic)); - BOOST_LOG(debug) - << "--begin unicode packet--"sv << std::endl - << "text ["sv << text << ']' << std::endl - << "--end unicode packet--"sv; -} - -void print(PNV_MULTI_CONTROLLER_PACKET packet) { - // Moonlight spams controller packet even when not necessary - BOOST_LOG(verbose) - << "--begin controller packet--"sv << std::endl - << "controllerNumber ["sv << packet->controllerNumber << ']' << std::endl - << "activeGamepadMask ["sv << util::hex(packet->activeGamepadMask).to_string_view() << ']' << std::endl - << "buttonFlags ["sv << util::hex(packet->buttonFlags).to_string_view() << ']' << std::endl - << "leftTrigger ["sv << util::hex(packet->leftTrigger).to_string_view() << ']' << std::endl - << "rightTrigger ["sv << util::hex(packet->rightTrigger).to_string_view() << ']' << std::endl - << "leftStickX ["sv << packet->leftStickX << ']' << std::endl - << "leftStickY ["sv << packet->leftStickY << ']' << std::endl - << "rightStickX ["sv << packet->rightStickX << ']' << std::endl - << "rightStickY ["sv << packet->rightStickY << ']' << std::endl - << "--end controller packet--"sv; -} - -void print(void *payload) { - auto header = (PNV_INPUT_HEADER)payload; - - switch(util::endian::little(header->magic)) { - case MOUSE_MOVE_REL_MAGIC_GEN5: - print((PNV_REL_MOUSE_MOVE_PACKET)payload); - break; - case MOUSE_MOVE_ABS_MAGIC: - print((PNV_ABS_MOUSE_MOVE_PACKET)payload); - break; - case MOUSE_BUTTON_DOWN_EVENT_MAGIC_GEN5: - case MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5: - print((PNV_MOUSE_BUTTON_PACKET)payload); - break; - case SCROLL_MAGIC_GEN5: - print((PNV_SCROLL_PACKET)payload); - break; - case SS_HSCROLL_MAGIC: - print((PSS_HSCROLL_PACKET)payload); - break; - case KEY_DOWN_EVENT_MAGIC: - case KEY_UP_EVENT_MAGIC: - print((PNV_KEYBOARD_PACKET)payload); - break; - case UTF8_TEXT_EVENT_MAGIC: - print((PNV_UNICODE_PACKET)payload); - break; - case MULTI_CONTROLLER_MAGIC_GEN5: - print((PNV_MULTI_CONTROLLER_PACKET)payload); - break; - } -} - -void passthrough(std::shared_ptr &input, PNV_REL_MOUSE_MOVE_PACKET packet) { - if(!config::input.mouse) { - return; + void + print(PNV_ABS_MOUSE_MOVE_PACKET packet) { + BOOST_LOG(debug) + << "--begin absolute mouse move packet--"sv << std::endl + << "x ["sv << util::endian::big(packet->x) << ']' << std::endl + << "y ["sv << util::endian::big(packet->y) << ']' << std::endl + << "width ["sv << util::endian::big(packet->width) << ']' << std::endl + << "height ["sv << util::endian::big(packet->height) << ']' << std::endl + << "--end absolute mouse move packet--"sv; } - input->mouse_left_button_timeout = DISABLE_LEFT_BUTTON_DELAY; - platf::move_mouse(platf_input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY)); -} - -void passthrough(std::shared_ptr &input, PNV_ABS_MOUSE_MOVE_PACKET packet) { - if(!config::input.mouse) { - return; + void + print(PNV_MOUSE_BUTTON_PACKET packet) { + BOOST_LOG(debug) + << "--begin mouse button packet--"sv << std::endl + << "action ["sv << util::hex(packet->header.magic).to_string_view() << ']' << std::endl + << "button ["sv << util::hex(packet->button).to_string_view() << ']' << std::endl + << "--end mouse button packet--"sv; } - if(input->mouse_left_button_timeout == DISABLE_LEFT_BUTTON_DELAY) { - input->mouse_left_button_timeout = ENABLE_LEFT_BUTTON_DELAY; + void + print(PNV_SCROLL_PACKET packet) { + BOOST_LOG(debug) + << "--begin mouse scroll packet--"sv << std::endl + << "scrollAmt1 ["sv << util::endian::big(packet->scrollAmt1) << ']' << std::endl + << "--end mouse scroll packet--"sv; } - auto &touch_port_event = input->touch_port_event; - auto &touch_port = input->touch_port; - if(touch_port_event->peek()) { - touch_port = *touch_port_event->pop(); + void + print(PSS_HSCROLL_PACKET packet) { + BOOST_LOG(debug) + << "--begin mouse hscroll packet--"sv << std::endl + << "scrollAmount ["sv << util::endian::big(packet->scrollAmount) << ']' << std::endl + << "--end mouse hscroll packet--"sv; } - float x = util::endian::big(packet->x); - float y = util::endian::big(packet->y); - - // Prevent divide by zero - // Don't expect it to happen, but just in case - if(!packet->width || !packet->height) { - BOOST_LOG(warning) << "Moonlight passed invalid dimensions"sv; - - return; + void + print(PNV_KEYBOARD_PACKET packet) { + BOOST_LOG(debug) + << "--begin keyboard packet--"sv << std::endl + << "keyAction ["sv << util::hex(packet->header.magic).to_string_view() << ']' << std::endl + << "keyCode ["sv << util::hex(packet->keyCode).to_string_view() << ']' << std::endl + << "modifiers ["sv << util::hex(packet->modifiers).to_string_view() << ']' << std::endl + << "flags ["sv << util::hex(packet->flags).to_string_view() << ']' << std::endl + << "--end keyboard packet--"sv; } - auto width = (float)util::endian::big(packet->width); - auto height = (float)util::endian::big(packet->height); - - auto scalarX = touch_port.width / width; - auto scalarY = touch_port.height / height; - - x *= scalarX; - y *= scalarY; - - auto offsetX = touch_port.client_offsetX; - auto offsetY = touch_port.client_offsetY; - - std::clamp(x, offsetX, width - offsetX); - std::clamp(y, offsetY, height - offsetY); - - platf::touch_port_t abs_port { - touch_port.offset_x, touch_port.offset_y, - touch_port.env_width, touch_port.env_height - }; - - platf::abs_mouse(platf_input, abs_port, (x - offsetX) * touch_port.scalar_inv, (y - offsetY) * touch_port.scalar_inv); -} - -void passthrough(std::shared_ptr &input, PNV_MOUSE_BUTTON_PACKET packet) { - if(!config::input.mouse) { - return; + void + print(PNV_UNICODE_PACKET packet) { + std::string text(packet->text, util::endian::big(packet->header.size) - sizeof(packet->header.magic)); + BOOST_LOG(debug) + << "--begin unicode packet--"sv << std::endl + << "text ["sv << text << ']' << std::endl + << "--end unicode packet--"sv; } - auto release = util::endian::little(packet->header.magic) == MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5; - auto button = util::endian::big(packet->button); - if(button > 0 && button < mouse_press.size()) { - if(mouse_press[button] != release) { - // button state is already what we want + void + print(PNV_MULTI_CONTROLLER_PACKET packet) { + // Moonlight spams controller packet even when not necessary + BOOST_LOG(verbose) + << "--begin controller packet--"sv << std::endl + << "controllerNumber ["sv << packet->controllerNumber << ']' << std::endl + << "activeGamepadMask ["sv << util::hex(packet->activeGamepadMask).to_string_view() << ']' << std::endl + << "buttonFlags ["sv << util::hex(packet->buttonFlags).to_string_view() << ']' << std::endl + << "leftTrigger ["sv << util::hex(packet->leftTrigger).to_string_view() << ']' << std::endl + << "rightTrigger ["sv << util::hex(packet->rightTrigger).to_string_view() << ']' << std::endl + << "leftStickX ["sv << packet->leftStickX << ']' << std::endl + << "leftStickY ["sv << packet->leftStickY << ']' << std::endl + << "rightStickX ["sv << packet->rightStickX << ']' << std::endl + << "rightStickY ["sv << packet->rightStickY << ']' << std::endl + << "--end controller packet--"sv; + } + + void + print(void *payload) { + auto header = (PNV_INPUT_HEADER) payload; + + switch (util::endian::little(header->magic)) { + case MOUSE_MOVE_REL_MAGIC_GEN5: + print((PNV_REL_MOUSE_MOVE_PACKET) payload); + break; + case MOUSE_MOVE_ABS_MAGIC: + print((PNV_ABS_MOUSE_MOVE_PACKET) payload); + break; + case MOUSE_BUTTON_DOWN_EVENT_MAGIC_GEN5: + case MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5: + print((PNV_MOUSE_BUTTON_PACKET) payload); + break; + case SCROLL_MAGIC_GEN5: + print((PNV_SCROLL_PACKET) payload); + break; + case SS_HSCROLL_MAGIC: + print((PSS_HSCROLL_PACKET) payload); + break; + case KEY_DOWN_EVENT_MAGIC: + case KEY_UP_EVENT_MAGIC: + print((PNV_KEYBOARD_PACKET) payload); + break; + case UTF8_TEXT_EVENT_MAGIC: + print((PNV_UNICODE_PACKET) payload); + break; + case MULTI_CONTROLLER_MAGIC_GEN5: + print((PNV_MULTI_CONTROLLER_PACKET) payload); + break; + } + } + + void + passthrough(std::shared_ptr &input, PNV_REL_MOUSE_MOVE_PACKET packet) { + if (!config::input.mouse) { return; } - mouse_press[button] = !release; + input->mouse_left_button_timeout = DISABLE_LEFT_BUTTON_DELAY; + platf::move_mouse(platf_input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY)); } - /////////////////////////////////// - /*/ + + void + passthrough(std::shared_ptr &input, PNV_ABS_MOUSE_MOVE_PACKET packet) { + if (!config::input.mouse) { + return; + } + + if (input->mouse_left_button_timeout == DISABLE_LEFT_BUTTON_DELAY) { + input->mouse_left_button_timeout = ENABLE_LEFT_BUTTON_DELAY; + } + + auto &touch_port_event = input->touch_port_event; + auto &touch_port = input->touch_port; + if (touch_port_event->peek()) { + touch_port = *touch_port_event->pop(); + } + + float x = util::endian::big(packet->x); + float y = util::endian::big(packet->y); + + // Prevent divide by zero + // Don't expect it to happen, but just in case + if (!packet->width || !packet->height) { + BOOST_LOG(warning) << "Moonlight passed invalid dimensions"sv; + + return; + } + + auto width = (float) util::endian::big(packet->width); + auto height = (float) util::endian::big(packet->height); + + auto scalarX = touch_port.width / width; + auto scalarY = touch_port.height / height; + + x *= scalarX; + y *= scalarY; + + auto offsetX = touch_port.client_offsetX; + auto offsetY = touch_port.client_offsetY; + + std::clamp(x, offsetX, width - offsetX); + std::clamp(y, offsetY, height - offsetY); + + platf::touch_port_t abs_port { + touch_port.offset_x, touch_port.offset_y, + touch_port.env_width, touch_port.env_height + }; + + platf::abs_mouse(platf_input, abs_port, (x - offsetX) * touch_port.scalar_inv, (y - offsetY) * touch_port.scalar_inv); + } + + void + passthrough(std::shared_ptr &input, PNV_MOUSE_BUTTON_PACKET packet) { + if (!config::input.mouse) { + return; + } + + auto release = util::endian::little(packet->header.magic) == MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5; + auto button = util::endian::big(packet->button); + if (button > 0 && button < mouse_press.size()) { + if (mouse_press[button] != release) { + // button state is already what we want + return; + } + + mouse_press[button] = !release; + } + /////////////////////////////////// + /*/ * When Moonlight sends mouse input through absolute coordinates, * it's possible that BUTTON_RIGHT is pressed down immediately after releasing BUTTON_LEFT. * As a result, Sunshine will left-click on hyperlinks in the browser before right-clicking @@ -350,389 +367,403 @@ void passthrough(std::shared_ptr &input, PNV_MOUSE_BUTTON_PACKET packet * input->mouse_left_button_timeout can only be nullptr * when the last mouse coordinates were absolute /*/ - if(button == BUTTON_LEFT && release && !input->mouse_left_button_timeout) { - auto f = [=]() { - auto left_released = mouse_press[BUTTON_LEFT]; - if(left_released) { - // Already released left button - return; - } - platf::button_mouse(platf_input, BUTTON_LEFT, release); + if (button == BUTTON_LEFT && release && !input->mouse_left_button_timeout) { + auto f = [=]() { + auto left_released = mouse_press[BUTTON_LEFT]; + if (left_released) { + // Already released left button + return; + } + platf::button_mouse(platf_input, BUTTON_LEFT, release); - mouse_press[BUTTON_LEFT] = false; - input->mouse_left_button_timeout = nullptr; - }; + mouse_press[BUTTON_LEFT] = false; + input->mouse_left_button_timeout = nullptr; + }; - input->mouse_left_button_timeout = task_pool.pushDelayed(std::move(f), 10ms).task_id; + input->mouse_left_button_timeout = task_pool.pushDelayed(std::move(f), 10ms).task_id; - return; - } - if( - button == BUTTON_RIGHT && !release && - input->mouse_left_button_timeout > DISABLE_LEFT_BUTTON_DELAY) { - platf::button_mouse(platf_input, BUTTON_RIGHT, false); - platf::button_mouse(platf_input, BUTTON_RIGHT, true); - - mouse_press[BUTTON_RIGHT] = false; - - return; - } - /////////////////////////////////// - - platf::button_mouse(platf_input, button, release); -} - -short map_keycode(short keycode) { - auto it = config::input.keybindings.find(keycode); - if(it != std::end(config::input.keybindings)) { - return it->second; - } - - return keycode; -} - -/** - * Update flags for keyboard shortcut combo's - */ -inline void update_shortcutFlags(int *flags, short keyCode, bool release) { - switch(keyCode) { - case VKEY_SHIFT: - case VKEY_LSHIFT: - case VKEY_RSHIFT: - if(release) { - *flags &= ~input_t::SHIFT; - } - else { - *flags |= input_t::SHIFT; - } - break; - case VKEY_CONTROL: - case VKEY_LCONTROL: - case VKEY_RCONTROL: - if(release) { - *flags &= ~input_t::CTRL; - } - else { - *flags |= input_t::CTRL; - } - break; - case VKEY_MENU: - case VKEY_LMENU: - case VKEY_RMENU: - if(release) { - *flags &= ~input_t::ALT; - } - else { - *flags |= input_t::ALT; - } - break; - } -} - -void repeat_key(short key_code) { - // If key no longer pressed, stop repeating - if(!key_press[key_code]) { - key_press_repeat_id = nullptr; - return; - } - - platf::keyboard(platf_input, map_keycode(key_code), false); - - key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code).task_id; -} - -void passthrough(std::shared_ptr &input, PNV_KEYBOARD_PACKET packet) { - if(!config::input.keyboard) { - return; - } - - auto release = util::endian::little(packet->header.magic) == KEY_UP_EVENT_MAGIC; - auto keyCode = packet->keyCode & 0x00FF; - - auto &pressed = key_press[keyCode]; - if(!pressed) { - if(!release) { - // A new key has been pressed down, we need to check for key combo's - // If a key-combo has been pressed down, don't pass it through - if(input->shortcutFlags == input_t::SHORTCUT && apply_shortcut(keyCode) > 0) { - return; - } - - if(key_press_repeat_id) { - task_pool.cancel(key_press_repeat_id); - } - - if(config::input.key_repeat_delay.count() > 0) { - key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_delay, keyCode).task_id; - } - } - else { - // Already released return; } - } - else if(!release) { - // Already pressed down key - return; + if ( + button == BUTTON_RIGHT && !release && + input->mouse_left_button_timeout > DISABLE_LEFT_BUTTON_DELAY) { + platf::button_mouse(platf_input, BUTTON_RIGHT, false); + platf::button_mouse(platf_input, BUTTON_RIGHT, true); + + mouse_press[BUTTON_RIGHT] = false; + + return; + } + /////////////////////////////////// + + platf::button_mouse(platf_input, button, release); } - pressed = !release; + short + map_keycode(short keycode) { + auto it = config::input.keybindings.find(keycode); + if (it != std::end(config::input.keybindings)) { + return it->second; + } - update_shortcutFlags(&input->shortcutFlags, map_keycode(keyCode), release); - platf::keyboard(platf_input, map_keycode(keyCode), release); -} - -void passthrough(PNV_SCROLL_PACKET packet) { - if(!config::input.mouse) { - return; + return keycode; } - platf::scroll(platf_input, util::endian::big(packet->scrollAmt1)); -} - -void passthrough(PSS_HSCROLL_PACKET packet) { - if(!config::input.mouse) { - return; + /** + * Update flags for keyboard shortcut combo's + */ + inline void + update_shortcutFlags(int *flags, short keyCode, bool release) { + switch (keyCode) { + case VKEY_SHIFT: + case VKEY_LSHIFT: + case VKEY_RSHIFT: + if (release) { + *flags &= ~input_t::SHIFT; + } + else { + *flags |= input_t::SHIFT; + } + break; + case VKEY_CONTROL: + case VKEY_LCONTROL: + case VKEY_RCONTROL: + if (release) { + *flags &= ~input_t::CTRL; + } + else { + *flags |= input_t::CTRL; + } + break; + case VKEY_MENU: + case VKEY_LMENU: + case VKEY_RMENU: + if (release) { + *flags &= ~input_t::ALT; + } + else { + *flags |= input_t::ALT; + } + break; + } } - platf::hscroll(platf_input, util::endian::big(packet->scrollAmount)); -} + void + repeat_key(short key_code) { + // If key no longer pressed, stop repeating + if (!key_press[key_code]) { + key_press_repeat_id = nullptr; + return; + } -void passthrough(PNV_UNICODE_PACKET packet) { - if(!config::input.keyboard) { - return; + platf::keyboard(platf_input, map_keycode(key_code), false); + + key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code).task_id; } - auto size = util::endian::big(packet->header.size) - sizeof(packet->header.magic); - platf::unicode(platf_input, packet->text, size); -} + void + passthrough(std::shared_ptr &input, PNV_KEYBOARD_PACKET packet) { + if (!config::input.keyboard) { + return; + } + + auto release = util::endian::little(packet->header.magic) == KEY_UP_EVENT_MAGIC; + auto keyCode = packet->keyCode & 0x00FF; + + auto &pressed = key_press[keyCode]; + if (!pressed) { + if (!release) { + // A new key has been pressed down, we need to check for key combo's + // If a key-combo has been pressed down, don't pass it through + if (input->shortcutFlags == input_t::SHORTCUT && apply_shortcut(keyCode) > 0) { + return; + } + + if (key_press_repeat_id) { + task_pool.cancel(key_press_repeat_id); + } + + if (config::input.key_repeat_delay.count() > 0) { + key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_delay, keyCode).task_id; + } + } + else { + // Already released + return; + } + } + else if (!release) { + // Already pressed down key + return; + } + + pressed = !release; + + update_shortcutFlags(&input->shortcutFlags, map_keycode(keyCode), release); + platf::keyboard(platf_input, map_keycode(keyCode), release); + } + + void + passthrough(PNV_SCROLL_PACKET packet) { + if (!config::input.mouse) { + return; + } + + platf::scroll(platf_input, util::endian::big(packet->scrollAmt1)); + } + + void + passthrough(PSS_HSCROLL_PACKET packet) { + if (!config::input.mouse) { + return; + } + + platf::hscroll(platf_input, util::endian::big(packet->scrollAmount)); + } + + void + passthrough(PNV_UNICODE_PACKET packet) { + if (!config::input.keyboard) { + return; + } + + auto size = util::endian::big(packet->header.size) - sizeof(packet->header.magic); + platf::unicode(platf_input, packet->text, size); + } + + int + updateGamepads(std::vector &gamepads, std::int16_t old_state, std::int16_t new_state, const platf::rumble_queue_t &rumble_queue) { + auto xorGamepadMask = old_state ^ new_state; + if (!xorGamepadMask) { + return 0; + } + + for (int x = 0; x < sizeof(std::int16_t) * 8; ++x) { + if ((xorGamepadMask >> x) & 1) { + auto &gamepad = gamepads[x]; + + if ((old_state >> x) & 1) { + if (gamepad.id < 0) { + return -1; + } + + free_gamepad(platf_input, gamepad.id); + gamepad.id = -1; + } + else { + auto id = alloc_id(gamepadMask); + + if (id < 0) { + // Out of gamepads + return -1; + } + + if (platf::alloc_gamepad(platf_input, id, rumble_queue)) { + free_id(gamepadMask, id); + // allocating a gamepad failed: solution: ignore gamepads + // The implementations of platf::alloc_gamepad already has logging + return -1; + } + + gamepad.id = id; + } + } + } -int updateGamepads(std::vector &gamepads, std::int16_t old_state, std::int16_t new_state, const platf::rumble_queue_t &rumble_queue) { - auto xorGamepadMask = old_state ^ new_state; - if(!xorGamepadMask) { return 0; } - for(int x = 0; x < sizeof(std::int16_t) * 8; ++x) { - if((xorGamepadMask >> x) & 1) { - auto &gamepad = gamepads[x]; + void + passthrough(std::shared_ptr &input, PNV_MULTI_CONTROLLER_PACKET packet) { + if (!config::input.controller) { + return; + } - if((old_state >> x) & 1) { - if(gamepad.id < 0) { - return -1; + if (updateGamepads(input->gamepads, input->active_gamepad_state, packet->activeGamepadMask, input->rumble_queue)) { + return; + } + + input->active_gamepad_state = packet->activeGamepadMask; + + if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { + BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; + + return; + } + + if (!((input->active_gamepad_state >> packet->controllerNumber) & 1)) { + BOOST_LOG(warning) << "ControllerNumber ["sv << packet->controllerNumber << "] not allocated"sv; + + return; + } + + auto &gamepad = input->gamepads[packet->controllerNumber]; + + // If this gamepad has not been initialized, ignore it. + // This could happen when platf::alloc_gamepad fails + if (gamepad.id < 0) { + return; + } + + std::uint16_t bf = packet->buttonFlags; + platf::gamepad_state_t gamepad_state { + bf, + packet->leftTrigger, + packet->rightTrigger, + packet->leftStickX, + packet->leftStickY, + packet->rightStickX, + packet->rightStickY + }; + + auto bf_new = gamepad_state.buttonFlags; + switch (gamepad.back_button_state) { + case button_state_e::UP: + if (!(platf::BACK & bf_new)) { + gamepad.back_button_state = button_state_e::NONE; } + gamepad_state.buttonFlags &= ~platf::BACK; + break; + case button_state_e::DOWN: + if (platf::BACK & bf_new) { + gamepad.back_button_state = button_state_e::NONE; + } + gamepad_state.buttonFlags |= platf::BACK; + break; + case button_state_e::NONE: + break; + } - free_gamepad(platf_input, gamepad.id); - gamepad.id = -1; + bf = gamepad_state.buttonFlags ^ gamepad.gamepad_state.buttonFlags; + bf_new = gamepad_state.buttonFlags; + + if (platf::BACK & bf) { + if (platf::BACK & bf_new) { + // Don't emulate home button if timeout < 0 + if (config::input.back_button_timeout >= 0ms) { + auto f = [input, controller = packet->controllerNumber]() { + auto &gamepad = input->gamepads[controller]; + + auto &state = gamepad.gamepad_state; + + // Force the back button up + gamepad.back_button_state = button_state_e::UP; + state.buttonFlags &= ~platf::BACK; + platf::gamepad(platf_input, gamepad.id, state); + + // Press Home button + state.buttonFlags |= platf::HOME; + platf::gamepad(platf_input, gamepad.id, state); + + // Release Home button + state.buttonFlags &= ~platf::HOME; + platf::gamepad(platf_input, gamepad.id, state); + + gamepad.back_timeout_id = nullptr; + }; + + gamepad.back_timeout_id = task_pool.pushDelayed(std::move(f), config::input.back_button_timeout).task_id; + } } - else { - auto id = alloc_id(gamepadMask); - - if(id < 0) { - // Out of gamepads - return -1; - } - - if(platf::alloc_gamepad(platf_input, id, rumble_queue)) { - free_id(gamepadMask, id); - // allocating a gamepad failed: solution: ignore gamepads - // The implementations of platf::alloc_gamepad already has logging - return -1; - } - - gamepad.id = id; + else if (gamepad.back_timeout_id) { + task_pool.cancel(gamepad.back_timeout_id); + gamepad.back_timeout_id = nullptr; } } + + platf::gamepad(platf_input, gamepad.id, gamepad_state); + + gamepad.gamepad_state = gamepad_state; + } + + void + passthrough_helper(std::shared_ptr input, std::vector &&input_data) { + void *payload = input_data.data(); + auto header = (PNV_INPUT_HEADER) payload; + + switch (util::endian::little(header->magic)) { + case MOUSE_MOVE_REL_MAGIC_GEN5: + passthrough(input, (PNV_REL_MOUSE_MOVE_PACKET) payload); + break; + case MOUSE_MOVE_ABS_MAGIC: + passthrough(input, (PNV_ABS_MOUSE_MOVE_PACKET) payload); + break; + case MOUSE_BUTTON_DOWN_EVENT_MAGIC_GEN5: + case MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5: + passthrough(input, (PNV_MOUSE_BUTTON_PACKET) payload); + break; + case SCROLL_MAGIC_GEN5: + passthrough((PNV_SCROLL_PACKET) payload); + break; + case SS_HSCROLL_MAGIC: + passthrough((PSS_HSCROLL_PACKET) payload); + break; + case KEY_DOWN_EVENT_MAGIC: + case KEY_UP_EVENT_MAGIC: + passthrough(input, (PNV_KEYBOARD_PACKET) payload); + break; + case UTF8_TEXT_EVENT_MAGIC: + passthrough((PNV_UNICODE_PACKET) payload); + break; + case MULTI_CONTROLLER_MAGIC_GEN5: + passthrough(input, (PNV_MULTI_CONTROLLER_PACKET) payload); + break; + } } - return 0; -} - -void passthrough(std::shared_ptr &input, PNV_MULTI_CONTROLLER_PACKET packet) { - if(!config::input.controller) { - return; + void + passthrough(std::shared_ptr &input, std::vector &&input_data) { + task_pool.push(passthrough_helper, input, move_by_copy_util::cmove(input_data)); } - if(updateGamepads(input->gamepads, input->active_gamepad_state, packet->activeGamepadMask, input->rumble_queue)) { - return; + void + reset(std::shared_ptr &input) { + task_pool.cancel(key_press_repeat_id); + task_pool.cancel(input->mouse_left_button_timeout); + + // Ensure input is synchronous, by using the task_pool + task_pool.push([]() { + for (int x = 0; x < mouse_press.size(); ++x) { + if (mouse_press[x]) { + platf::button_mouse(platf_input, x, true); + mouse_press[x] = false; + } + } + + for (auto &kp : key_press) { + platf::keyboard(platf_input, kp.first & 0x00FF, true); + key_press[kp.first] = false; + } + }); } - input->active_gamepad_state = packet->activeGamepadMask; - - if(packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { - BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; - - return; - } - - if(!((input->active_gamepad_state >> packet->controllerNumber) & 1)) { - BOOST_LOG(warning) << "ControllerNumber ["sv << packet->controllerNumber << "] not allocated"sv; - - return; - } - - auto &gamepad = input->gamepads[packet->controllerNumber]; - - // If this gamepad has not been initialized, ignore it. - // This could happen when platf::alloc_gamepad fails - if(gamepad.id < 0) { - return; - } - - std::uint16_t bf = packet->buttonFlags; - platf::gamepad_state_t gamepad_state { - bf, - packet->leftTrigger, - packet->rightTrigger, - packet->leftStickX, - packet->leftStickY, - packet->rightStickX, - packet->rightStickY + class deinit_t: public platf::deinit_t { + public: + ~deinit_t() override { + platf_input.reset(); + } }; - auto bf_new = gamepad_state.buttonFlags; - switch(gamepad.back_button_state) { - case button_state_e::UP: - if(!(platf::BACK & bf_new)) { - gamepad.back_button_state = button_state_e::NONE; - } - gamepad_state.buttonFlags &= ~platf::BACK; - break; - case button_state_e::DOWN: - if(platf::BACK & bf_new) { - gamepad.back_button_state = button_state_e::NONE; - } - gamepad_state.buttonFlags |= platf::BACK; - break; - case button_state_e::NONE: - break; + [[nodiscard]] std::unique_ptr + init() { + platf_input = platf::input(); + + return std::make_unique(); } - bf = gamepad_state.buttonFlags ^ gamepad.gamepad_state.buttonFlags; - bf_new = gamepad_state.buttonFlags; + std::shared_ptr + alloc(safe::mail_t mail) { + auto input = std::make_shared( + mail->event(mail::touch_port), + mail->queue(mail::rumble)); - if(platf::BACK & bf) { - if(platf::BACK & bf_new) { - // Don't emulate home button if timeout < 0 - if(config::input.back_button_timeout >= 0ms) { - auto f = [input, controller = packet->controllerNumber]() { - auto &gamepad = input->gamepads[controller]; + // Workaround to ensure new frames will be captured when a client connects + task_pool.pushDelayed([]() { + platf::move_mouse(platf_input, 1, 1); + platf::move_mouse(platf_input, -1, -1); + }, + 100ms); - auto &state = gamepad.gamepad_state; - - // Force the back button up - gamepad.back_button_state = button_state_e::UP; - state.buttonFlags &= ~platf::BACK; - platf::gamepad(platf_input, gamepad.id, state); - - // Press Home button - state.buttonFlags |= platf::HOME; - platf::gamepad(platf_input, gamepad.id, state); - - // Release Home button - state.buttonFlags &= ~platf::HOME; - platf::gamepad(platf_input, gamepad.id, state); - - gamepad.back_timeout_id = nullptr; - }; - - gamepad.back_timeout_id = task_pool.pushDelayed(std::move(f), config::input.back_button_timeout).task_id; - } - } - else if(gamepad.back_timeout_id) { - task_pool.cancel(gamepad.back_timeout_id); - gamepad.back_timeout_id = nullptr; - } + return input; } - - platf::gamepad(platf_input, gamepad.id, gamepad_state); - - gamepad.gamepad_state = gamepad_state; -} - -void passthrough_helper(std::shared_ptr input, std::vector &&input_data) { - void *payload = input_data.data(); - auto header = (PNV_INPUT_HEADER)payload; - - switch(util::endian::little(header->magic)) { - case MOUSE_MOVE_REL_MAGIC_GEN5: - passthrough(input, (PNV_REL_MOUSE_MOVE_PACKET)payload); - break; - case MOUSE_MOVE_ABS_MAGIC: - passthrough(input, (PNV_ABS_MOUSE_MOVE_PACKET)payload); - break; - case MOUSE_BUTTON_DOWN_EVENT_MAGIC_GEN5: - case MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5: - passthrough(input, (PNV_MOUSE_BUTTON_PACKET)payload); - break; - case SCROLL_MAGIC_GEN5: - passthrough((PNV_SCROLL_PACKET)payload); - break; - case SS_HSCROLL_MAGIC: - passthrough((PSS_HSCROLL_PACKET)payload); - break; - case KEY_DOWN_EVENT_MAGIC: - case KEY_UP_EVENT_MAGIC: - passthrough(input, (PNV_KEYBOARD_PACKET)payload); - break; - case UTF8_TEXT_EVENT_MAGIC: - passthrough((PNV_UNICODE_PACKET)payload); - break; - case MULTI_CONTROLLER_MAGIC_GEN5: - passthrough(input, (PNV_MULTI_CONTROLLER_PACKET)payload); - break; - } -} - -void passthrough(std::shared_ptr &input, std::vector &&input_data) { - task_pool.push(passthrough_helper, input, move_by_copy_util::cmove(input_data)); -} - -void reset(std::shared_ptr &input) { - task_pool.cancel(key_press_repeat_id); - task_pool.cancel(input->mouse_left_button_timeout); - - // Ensure input is synchronous, by using the task_pool - task_pool.push([]() { - for(int x = 0; x < mouse_press.size(); ++x) { - if(mouse_press[x]) { - platf::button_mouse(platf_input, x, true); - mouse_press[x] = false; - } - } - - for(auto &kp : key_press) { - platf::keyboard(platf_input, kp.first & 0x00FF, true); - key_press[kp.first] = false; - } - }); -} - -class deinit_t : public platf::deinit_t { -public: - ~deinit_t() override { - platf_input.reset(); - } -}; - -[[nodiscard]] std::unique_ptr init() { - platf_input = platf::input(); - - return std::make_unique(); -} - -std::shared_ptr alloc(safe::mail_t mail) { - auto input = std::make_shared( - mail->event(mail::touch_port), - mail->queue(mail::rumble)); - - // Workaround to ensure new frames will be captured when a client connects - task_pool.pushDelayed([]() { - platf::move_mouse(platf_input, 1, 1); - platf::move_mouse(platf_input, -1, -1); - }, - 100ms); - - return input; -} -} // namespace input +} // namespace input diff --git a/src/input.h b/src/input.h index ce291623..2001fb9e 100644 --- a/src/input.h +++ b/src/input.h @@ -9,25 +9,29 @@ #include "thread_safe.h" namespace input { -struct input_t; + struct input_t; -void print(void *input); -void reset(std::shared_ptr &input); -void passthrough(std::shared_ptr &input, std::vector &&input_data); + void + print(void *input); + void + reset(std::shared_ptr &input); + void + passthrough(std::shared_ptr &input, std::vector &&input_data); + [[nodiscard]] std::unique_ptr + init(); -[[nodiscard]] std::unique_ptr init(); + std::shared_ptr + alloc(safe::mail_t mail); -std::shared_ptr alloc(safe::mail_t mail); + struct touch_port_t: public platf::touch_port_t { + int env_width, env_height; -struct touch_port_t : public platf::touch_port_t { - int env_width, env_height; + // Offset x and y coordinates of the client + float client_offsetX, client_offsetY; - // Offset x and y coordinates of the client - float client_offsetX, client_offsetY; + float scalar_inv; + }; +} // namespace input - float scalar_inv; -}; -} // namespace input - -#endif // SUNSHINE_INPUT_H +#endif // SUNSHINE_INPUT_H diff --git a/src/main.cpp b/src/main.cpp index 50099207..991631fa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -42,12 +42,12 @@ using namespace std::literals; namespace bl = boost::log; thread_pool_util::ThreadPool task_pool; -bl::sources::severity_logger verbose(0); // Dominating output -bl::sources::severity_logger debug(1); // Follow what is happening -bl::sources::severity_logger info(2); // Should be informed about -bl::sources::severity_logger warning(3); // Strange events -bl::sources::severity_logger error(4); // Recoverable errors -bl::sources::severity_logger fatal(5); // Unrecoverable errors +bl::sources::severity_logger verbose(0); // Dominating output +bl::sources::severity_logger debug(1); // Follow what is happening +bl::sources::severity_logger info(2); // Should be informed about +bl::sources::severity_logger warning(3); // Strange events +bl::sources::severity_logger error(4); // Recoverable errors +bl::sources::severity_logger fatal(5); // Unrecoverable errors bool display_cursor = true; @@ -55,7 +55,8 @@ using text_sink = bl::sinks::asynchronous_sink; boost::shared_ptr sink; struct NoDelete { - void operator()(void *) {} + void + operator()(void *) {} }; BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", int) @@ -69,7 +70,8 @@ BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", int) * print_help("sunshine"); * ``` */ -void print_help(const char *name) { +void +print_help(const char *name) { std::cout << "Usage: "sv << name << " [options] [/path/to/configuration_file] [--cmd]"sv << std::endl << " Any configurable option can be overwritten with: \"name=value\""sv << std::endl @@ -90,19 +92,20 @@ void print_help(const char *name) { } namespace help { -int entry(const char *name, int argc, char *argv[]) { - print_help(name); - return 0; -} -} // namespace help + int + entry(const char *name, int argc, char *argv[]) { + print_help(name); + return 0; + } +} // namespace help namespace version { -int entry(const char *name, int argc, char *argv[]) { - std::cout << PROJECT_NAME << " version: v" << PROJECT_VER << std::endl; - return 0; -} -} // namespace version - + int + entry(const char *name, int argc, char *argv[]) { + std::cout << PROJECT_NAME << " version: v" << PROJECT_VER << std::endl; + return 0; + } +} // namespace version /** * @brief Flush the log. @@ -112,34 +115,38 @@ int entry(const char *name, int argc, char *argv[]) { * log_flush(); * ``` */ -void log_flush() { +void +log_flush() { sink->flush(); } std::map> signal_handlers; -void on_signal_forwarder(int sig) { +void +on_signal_forwarder(int sig) { signal_handlers.at(sig)(); } -template -void on_signal(int sig, FN &&fn) { +template +void +on_signal(int sig, FN &&fn) { signal_handlers.emplace(sig, std::forward(fn)); std::signal(sig, on_signal_forwarder); } namespace gen_creds { -int entry(const char *name, int argc, char *argv[]) { - if(argc < 2 || argv[0] == "help"sv || argv[1] == "help"sv) { - print_help(name); + int + entry(const char *name, int argc, char *argv[]) { + if (argc < 2 || argv[0] == "help"sv || argv[1] == "help"sv) { + print_help(name); + return 0; + } + + http::save_user_creds(config::sunshine.credentials_file, argv[0], argv[1]); + return 0; } - - http::save_user_creds(config::sunshine.credentials_file, argv[0], argv[1]); - - return 0; -} -} // namespace gen_creds +} // namespace gen_creds std::map> cmd_to_func { { "creds"sv, gen_creds::entry }, @@ -148,20 +155,21 @@ std::map 0) { + while (GetMessage(&msg, nullptr, 0, 0) > 0) { TranslateMessage(&msg); DispatchMessage(&msg); } @@ -225,11 +234,11 @@ int main(int argc, char *argv[]) { mail::man = std::make_shared(); - if(config::parse(argc, argv)) { + if (config::parse(argc, argv)) { return 0; } - if(config::sunshine.min_log_level >= 1) { + if (config::sunshine.min_log_level >= 1) { av_log_set_level(AV_LOG_QUIET); } else { @@ -244,30 +253,30 @@ int main(int argc, char *argv[]) { sink->set_filter(severity >= config::sunshine.min_log_level); sink->set_formatter([message = "Message"s, severity = "Severity"s](const bl::record_view &view, bl::formatting_ostream &os) { - constexpr int DATE_BUFFER_SIZE = 21 + 2 + 1; // Full string plus ": \0" + constexpr int DATE_BUFFER_SIZE = 21 + 2 + 1; // Full string plus ": \0" auto log_level = view.attribute_values()[severity].extract().get(); std::string_view log_type; - switch(log_level) { - case 0: - log_type = "Verbose: "sv; - break; - case 1: - log_type = "Debug: "sv; - break; - case 2: - log_type = "Info: "sv; - break; - case 3: - log_type = "Warning: "sv; - break; - case 4: - log_type = "Error: "sv; - break; - case 5: - log_type = "Fatal: "sv; - break; + switch (log_level) { + case 0: + log_type = "Verbose: "sv; + break; + case 1: + log_type = "Debug: "sv; + break; + case 2: + log_type = "Info: "sv; + break; + case 3: + log_type = "Warning: "sv; + break; + case 4: + log_type = "Error: "sv; + break; + case 5: + log_type = "Fatal: "sv; + break; }; char _date[DATE_BUFFER_SIZE]; @@ -284,13 +293,13 @@ int main(int argc, char *argv[]) { bl::core::get()->add_sink(sink); auto fg = util::fail_guard(log_flush); - if(!config::sunshine.cmd.name.empty()) { + if (!config::sunshine.cmd.name.empty()) { auto fn = cmd_to_func.find(config::sunshine.cmd.name); - if(fn == std::end(cmd_to_func)) { + if (fn == std::end(cmd_to_func)) { BOOST_LOG(fatal) << "Unknown command: "sv << config::sunshine.cmd.name; BOOST_LOG(info) << "Possible commands:"sv; - for(auto &[key, _] : cmd_to_func) { + for (auto &[key, _] : cmd_to_func) { BOOST_LOG(info) << '\t' << key; } @@ -338,16 +347,16 @@ int main(int argc, char *argv[]) { proc::refresh(config::stream.file_apps); auto deinit_guard = platf::init(); - if(!deinit_guard) { + if (!deinit_guard) { return 4; } reed_solomon_init(); auto input_deinit_guard = input::init(); - if(video::init()) { + if (video::init()) { return 2; } - if(http::init()) { + if (http::init()) { return 3; } @@ -362,7 +371,7 @@ int main(int argc, char *argv[]) { }); // FIXME: Temporary workaround: Simple-Web_server needs to be updated or replaced - if(shutdown_event->peek()) { + if (shutdown_event->peek()) { return 0; } @@ -395,8 +404,9 @@ int main(int argc, char *argv[]) { * std::string contents = read_file("path/to/file"); * ``` */ -std::string read_file(const char *path) { - if(!std::filesystem::exists(path)) { +std::string +read_file(const char *path) { + if (!std::filesystem::exists(path)) { BOOST_LOG(debug) << "Missing file: " << path; return {}; } @@ -406,7 +416,7 @@ std::string read_file(const char *path) { std::string input; std::string base64_cert; - while(!in.eof()) { + while (!in.eof()) { std::getline(in, input); base64_cert += input + '\n'; } @@ -425,10 +435,11 @@ std::string read_file(const char *path) { * int write_status = write_file("path/to/file", "file contents"); * ``` */ -int write_file(const char *path, const std::string_view &contents) { +int +write_file(const char *path, const std::string_view &contents) { std::ofstream out(path); - if(!out.is_open()) { + if (!out.is_open()) { return -1; } @@ -447,8 +458,9 @@ int write_file(const char *path, const std::string_view &contents) { * std::uint16_t mapped_port = map_port(1); * ``` */ -std::uint16_t map_port(int port) { +std::uint16_t +map_port(int port) { // TODO: Ensure port is in the range of 21-65535 // TODO: Ensure port is not already in use by another application - return (std::uint16_t)((int)config::sunshine.port + port); + return (std::uint16_t)((int) config::sunshine.port + port); } diff --git a/src/main.h b/src/main.h index 874c1fcf..aff1cfdd 100644 --- a/src/main.h +++ b/src/main.h @@ -28,42 +28,55 @@ extern boost::log::sources::severity_logger error; extern boost::log::sources::severity_logger fatal; // functions -int main(int argc, char *argv[]); -void log_flush(); -void open_url(const std::string &url); -void tray_open_ui_cb(struct tray_menu *item); -void tray_donate_github_cb(struct tray_menu *item); -void tray_donate_mee6_cb(struct tray_menu *item); -void tray_donate_patreon_cb(struct tray_menu *item); -void tray_donate_paypal_cb(struct tray_menu *item); -void tray_quit_cb(struct tray_menu *item); -void print_help(const char *name); -std::string read_file(const char *path); -int write_file(const char *path, const std::string_view &contents); -std::uint16_t map_port(int port); +int +main(int argc, char *argv[]); +void +log_flush(); +void +open_url(const std::string &url); +void +tray_open_ui_cb(struct tray_menu *item); +void +tray_donate_github_cb(struct tray_menu *item); +void +tray_donate_mee6_cb(struct tray_menu *item); +void +tray_donate_patreon_cb(struct tray_menu *item); +void +tray_donate_paypal_cb(struct tray_menu *item); +void +tray_quit_cb(struct tray_menu *item); +void +print_help(const char *name); +std::string +read_file(const char *path); +int +write_file(const char *path, const std::string_view &contents); +std::uint16_t +map_port(int port); // namespaces namespace mail { #define MAIL(x) \ constexpr auto x = std::string_view { \ -#x \ + #x \ } -extern safe::mail_t man; + extern safe::mail_t man; -// Global mail -MAIL(shutdown); -MAIL(broadcast_shutdown); -MAIL(video_packets); -MAIL(audio_packets); -MAIL(switch_display); + // Global mail + MAIL(shutdown); + MAIL(broadcast_shutdown); + MAIL(video_packets); + MAIL(audio_packets); + MAIL(switch_display); -// Local mail -MAIL(touch_port); -MAIL(idr); -MAIL(rumble); -MAIL(hdr); + // Local mail + MAIL(touch_port); + MAIL(idr); + MAIL(rumble); + MAIL(hdr); #undef MAIL -} // namespace mail -#endif // SUNSHINE_MAIN_H +} // namespace mail +#endif // SUNSHINE_MAIN_H diff --git a/src/move_by_copy.h b/src/move_by_copy.h index cc331798..3c2bda12 100644 --- a/src/move_by_copy.h +++ b/src/move_by_copy.h @@ -3,49 +3,54 @@ #include namespace move_by_copy_util { -/* + /* * When a copy is made, it moves the object * This allows you to move an object when a move can't be done. */ -template -class MoveByCopy { -public: - typedef T move_type; + template + class MoveByCopy { + public: + typedef T move_type; -private: - move_type _to_move; + private: + move_type _to_move; -public: - explicit MoveByCopy(move_type &&to_move) : _to_move(std::move(to_move)) {} + public: + explicit MoveByCopy(move_type &&to_move): + _to_move(std::move(to_move)) {} - MoveByCopy(MoveByCopy &&other) = default; + MoveByCopy(MoveByCopy &&other) = default; - MoveByCopy(const MoveByCopy &other) { - *this = other; + MoveByCopy(const MoveByCopy &other) { + *this = other; + } + + MoveByCopy & + operator=(MoveByCopy &&other) = default; + + MoveByCopy & + operator=(const MoveByCopy &other) { + this->_to_move = std::move(const_cast(other)._to_move); + + return *this; + } + + operator move_type() { + return std::move(_to_move); + } + }; + + template + MoveByCopy + cmove(T &movable) { + return MoveByCopy(std::move(movable)); } - MoveByCopy &operator=(MoveByCopy &&other) = default; - - MoveByCopy &operator=(const MoveByCopy &other) { - this->_to_move = std::move(const_cast(other)._to_move); - - return *this; + // Do NOT use this unless you are absolutely certain the object to be moved is no longer used by the caller + template + MoveByCopy + const_cmove(const T &movable) { + return MoveByCopy(std::move(const_cast(movable))); } - - operator move_type() { - return std::move(_to_move); - } -}; - -template -MoveByCopy cmove(T &movable) { - return MoveByCopy(std::move(movable)); -} - -// Do NOT use this unless you are absolutely certain the object to be moved is no longer used by the caller -template -MoveByCopy const_cmove(const T &movable) { - return MoveByCopy(std::move(const_cast(movable))); -} -} // namespace move_by_copy_util +} // namespace move_by_copy_util #endif diff --git a/src/network.cpp b/src/network.cpp index 90254899..6549127c 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -6,108 +6,116 @@ using namespace std::literals; namespace net { -// In the format "xxx.xxx.xxx.xxx/x" -std::pair ip_block(const std::string_view &ip); + // In the format "xxx.xxx.xxx.xxx/x" + std::pair + ip_block(const std::string_view &ip); -std::vector> pc_ips { - ip_block("127.0.0.1/32"sv) -}; -std::vector> lan_ips { - ip_block("192.168.0.0/16"sv), - ip_block("172.16.0.0/12"sv), - ip_block("10.0.0.0/8"sv) -}; + std::vector> pc_ips { + ip_block("127.0.0.1/32"sv) + }; + std::vector> lan_ips { + ip_block("192.168.0.0/16"sv), + ip_block("172.16.0.0/12"sv), + ip_block("10.0.0.0/8"sv) + }; -std::uint32_t ip(const std::string_view &ip_str) { - auto begin = std::begin(ip_str); - auto end = std::end(ip_str); - auto temp_end = std::find(begin, end, '.'); + std::uint32_t + ip(const std::string_view &ip_str) { + auto begin = std::begin(ip_str); + auto end = std::end(ip_str); + auto temp_end = std::find(begin, end, '.'); - std::uint32_t ip = 0; - auto shift = 24; - while(temp_end != end) { - ip += (util::from_chars(begin, temp_end) << shift); - shift -= 8; + std::uint32_t ip = 0; + auto shift = 24; + while (temp_end != end) { + ip += (util::from_chars(begin, temp_end) << shift); + shift -= 8; - begin = temp_end + 1; - temp_end = std::find(begin, end, '.'); - } - - ip += util::from_chars(begin, end); - - return ip; -} - -// In the format "xxx.xxx.xxx.xxx/x" -std::pair ip_block(const std::string_view &ip_str) { - auto begin = std::begin(ip_str); - auto end = std::find(begin, std::end(ip_str), '/'); - - auto addr = ip({ begin, (std::size_t)(end - begin) }); - - auto bits = 32 - util::from_chars(end + 1, std::end(ip_str)); - - return { addr, addr + ((1 << bits) - 1) }; -} - -net_e from_enum_string(const std::string_view &view) { - if(view == "wan") { - return WAN; - } - if(view == "lan") { - return LAN; - } - - return PC; -} -net_e from_address(const std::string_view &view) { - auto addr = ip(view); - - for(auto [ip_low, ip_high] : pc_ips) { - if(addr >= ip_low && addr <= ip_high) { - return PC; + begin = temp_end + 1; + temp_end = std::find(begin, end, '.'); } + + ip += util::from_chars(begin, end); + + return ip; } - for(auto [ip_low, ip_high] : lan_ips) { - if(addr >= ip_low && addr <= ip_high) { + // In the format "xxx.xxx.xxx.xxx/x" + std::pair + ip_block(const std::string_view &ip_str) { + auto begin = std::begin(ip_str); + auto end = std::find(begin, std::end(ip_str), '/'); + + auto addr = ip({ begin, (std::size_t)(end - begin) }); + + auto bits = 32 - util::from_chars(end + 1, std::end(ip_str)); + + return { addr, addr + ((1 << bits) - 1) }; + } + + net_e + from_enum_string(const std::string_view &view) { + if (view == "wan") { + return WAN; + } + if (view == "lan") { return LAN; } + + return PC; + } + net_e + from_address(const std::string_view &view) { + auto addr = ip(view); + + for (auto [ip_low, ip_high] : pc_ips) { + if (addr >= ip_low && addr <= ip_high) { + return PC; + } + } + + for (auto [ip_low, ip_high] : lan_ips) { + if (addr >= ip_low && addr <= ip_high) { + return LAN; + } + } + + return WAN; } - return WAN; -} + std::string_view + to_enum_string(net_e net) { + switch (net) { + case PC: + return "pc"sv; + case LAN: + return "lan"sv; + case WAN: + return "wan"sv; + } -std::string_view to_enum_string(net_e net) { - switch(net) { - case PC: - return "pc"sv; - case LAN: - return "lan"sv; - case WAN: + // avoid warning return "wan"sv; } - // avoid warning - return "wan"sv; -} + host_t + host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port) { + enet_address_set_host(&addr, "0.0.0.0"); + enet_address_set_port(&addr, port); -host_t host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port) { - enet_address_set_host(&addr, "0.0.0.0"); - enet_address_set_port(&addr, port); + return host_t { enet_host_create(AF_INET, &addr, peers, 1, 0, 0) }; + } - return host_t { enet_host_create(AF_INET, &addr, peers, 1, 0, 0) }; -} + void + free_host(ENetHost *host) { + std::for_each(host->peers, host->peers + host->peerCount, [](ENetPeer &peer_ref) { + ENetPeer *peer = &peer_ref; -void free_host(ENetHost *host) { - std::for_each(host->peers, host->peers + host->peerCount, [](ENetPeer &peer_ref) { - ENetPeer *peer = &peer_ref; + if (peer) { + enet_peer_disconnect_now(peer, 0); + } + }); - if(peer) { - enet_peer_disconnect_now(peer, 0); - } - }); - - enet_host_destroy(host); -} -} // namespace net + enet_host_destroy(host); + } +} // namespace net diff --git a/src/network.h b/src/network.h index bd371841..ffed2fe6 100644 --- a/src/network.h +++ b/src/network.h @@ -10,24 +10,29 @@ #include "utility.h" namespace net { -void free_host(ENetHost *host); + void + free_host(ENetHost *host); -using host_t = util::safe_ptr; -using peer_t = ENetPeer *; -using packet_t = util::safe_ptr; + using host_t = util::safe_ptr; + using peer_t = ENetPeer *; + using packet_t = util::safe_ptr; -enum net_e : int { - PC, - LAN, - WAN -}; + enum net_e : int { + PC, + LAN, + WAN + }; -net_e from_enum_string(const std::string_view &view); -std::string_view to_enum_string(net_e net); + net_e + from_enum_string(const std::string_view &view); + std::string_view + to_enum_string(net_e net); -net_e from_address(const std::string_view &view); + net_e + from_address(const std::string_view &view); -host_t host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port); -} // namespace net + host_t + host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port); +} // namespace net -#endif // SUNSHINE_NETWORK_H +#endif // SUNSHINE_NETWORK_H diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index cd87e8c6..15fc1f34 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -33,484 +33,498 @@ using namespace std::literals; namespace nvhttp { -namespace fs = std::filesystem; -namespace pt = boost::property_tree; + namespace fs = std::filesystem; + namespace pt = boost::property_tree; -class SunshineHttpsServer : public SimpleWeb::Server { -public: - SunshineHttpsServer(const std::string &certification_file, const std::string &private_key_file) - : SimpleWeb::Server::Server(certification_file, private_key_file) {} + class SunshineHttpsServer: public SimpleWeb::Server { + public: + SunshineHttpsServer(const std::string &certification_file, const std::string &private_key_file): + SimpleWeb::Server::Server(certification_file, private_key_file) {} - std::function verify; - std::function, std::shared_ptr)> on_verify_failed; + std::function verify; + std::function, std::shared_ptr)> on_verify_failed; -protected: - void after_bind() override { - SimpleWeb::Server::after_bind(); + protected: + void + after_bind() override { + SimpleWeb::Server::after_bind(); - if(verify) { - context.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once); - context.set_verify_callback([](int verified, boost::asio::ssl::verify_context &ctx) { - // To respond with an error message, a connection must be established - return 1; - }); - } - } - - // This is Server::accept() with SSL validation support added - void accept() override { - auto connection = create_connection(*io_service, context); - - acceptor->async_accept(connection->socket->lowest_layer(), [this, connection](const SimpleWeb::error_code &ec) { - auto lock = connection->handler_runner->continue_lock(); - if(!lock) - return; - - if(ec != SimpleWeb::error::operation_aborted) - this->accept(); - - auto session = std::make_shared(config.max_request_streambuf_size, connection); - - if(!ec) { - boost::asio::ip::tcp::no_delay option(true); - SimpleWeb::error_code ec; - session->connection->socket->lowest_layer().set_option(option, ec); - - session->connection->set_timeout(config.timeout_request); - session->connection->socket->async_handshake(boost::asio::ssl::stream_base::server, [this, session](const SimpleWeb::error_code &ec) { - session->connection->cancel_timeout(); - auto lock = session->connection->handler_runner->continue_lock(); - if(!lock) - return; - if(!ec) { - if(verify && !verify(session->connection->socket->native_handle())) - this->write(session, on_verify_failed); - else - this->read(session); - } - else if(this->on_error) - this->on_error(session->request, ec); + if (verify) { + context.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once); + context.set_verify_callback([](int verified, boost::asio::ssl::verify_context &ctx) { + // To respond with an error message, a connection must be established + return 1; }); } - else if(this->on_error) - this->on_error(session->request, ec); - }); - } -}; - -using https_server_t = SunshineHttpsServer; -using http_server_t = SimpleWeb::Server; - -struct conf_intern_t { - std::string servercert; - std::string pkey; -} conf_intern; - -struct client_t { - std::string uniqueID; - std::vector certs; -}; - -struct pair_session_t { - struct { - std::string uniqueID; - std::string cert; - } client; - - std::unique_ptr cipher_key; - std::vector clienthash; - - std::string serversecret; - std::string serverchallenge; - - struct { - util::Either< - std::shared_ptr::Response>, - std::shared_ptr::Response>> - response; - std::string salt; - } async_insert_pin; -}; - -// uniqueID, session -std::unordered_map map_id_sess; -std::unordered_map map_id_client; - -using args_t = SimpleWeb::CaseInsensitiveMultimap; -using resp_https_t = std::shared_ptr::Response>; -using req_https_t = std::shared_ptr::Request>; -using resp_http_t = std::shared_ptr::Response>; -using req_http_t = std::shared_ptr::Request>; - -enum class op_e { - ADD, - REMOVE -}; - -std::string get_arg(const args_t &args, const char *name) { - auto it = args.find(name); - if(it == std::end(args)) { - throw std::out_of_range(name); - } - return it->second; -} - -void save_state() { - pt::ptree root; - - if(fs::exists(config::nvhttp.file_state)) { - try { - pt::read_json(config::nvhttp.file_state, root); } - catch(std::exception &e) { - BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what(); + + // This is Server::accept() with SSL validation support added + void + accept() override { + auto connection = create_connection(*io_service, context); + + acceptor->async_accept(connection->socket->lowest_layer(), [this, connection](const SimpleWeb::error_code &ec) { + auto lock = connection->handler_runner->continue_lock(); + if (!lock) + return; + + if (ec != SimpleWeb::error::operation_aborted) + this->accept(); + + auto session = std::make_shared(config.max_request_streambuf_size, connection); + + if (!ec) { + boost::asio::ip::tcp::no_delay option(true); + SimpleWeb::error_code ec; + session->connection->socket->lowest_layer().set_option(option, ec); + + session->connection->set_timeout(config.timeout_request); + session->connection->socket->async_handshake(boost::asio::ssl::stream_base::server, [this, session](const SimpleWeb::error_code &ec) { + session->connection->cancel_timeout(); + auto lock = session->connection->handler_runner->continue_lock(); + if (!lock) + return; + if (!ec) { + if (verify && !verify(session->connection->socket->native_handle())) + this->write(session, on_verify_failed); + else + this->read(session); + } + else if (this->on_error) + this->on_error(session->request, ec); + }); + } + else if (this->on_error) + this->on_error(session->request, ec); + }); + } + }; + + using https_server_t = SunshineHttpsServer; + using http_server_t = SimpleWeb::Server; + + struct conf_intern_t { + std::string servercert; + std::string pkey; + } conf_intern; + + struct client_t { + std::string uniqueID; + std::vector certs; + }; + + struct pair_session_t { + struct { + std::string uniqueID; + std::string cert; + } client; + + std::unique_ptr cipher_key; + std::vector clienthash; + + std::string serversecret; + std::string serverchallenge; + + struct { + util::Either< + std::shared_ptr::Response>, + std::shared_ptr::Response>> + response; + std::string salt; + } async_insert_pin; + }; + + // uniqueID, session + std::unordered_map map_id_sess; + std::unordered_map map_id_client; + + using args_t = SimpleWeb::CaseInsensitiveMultimap; + using resp_https_t = std::shared_ptr::Response>; + using req_https_t = std::shared_ptr::Request>; + using resp_http_t = std::shared_ptr::Response>; + using req_http_t = std::shared_ptr::Request>; + + enum class op_e { + ADD, + REMOVE + }; + + std::string + get_arg(const args_t &args, const char *name) { + auto it = args.find(name); + if (it == std::end(args)) { + throw std::out_of_range(name); + } + return it->second; + } + + void + save_state() { + pt::ptree root; + + if (fs::exists(config::nvhttp.file_state)) { + try { + pt::read_json(config::nvhttp.file_state, root); + } + catch (std::exception &e) { + BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what(); + return; + } + } + + root.erase("root"s); + + root.put("root.uniqueid", http::unique_id); + auto &nodes = root.add_child("root.devices", pt::ptree {}); + for (auto &[_, client] : map_id_client) { + pt::ptree node; + + node.put("uniqueid"s, client.uniqueID); + + pt::ptree cert_nodes; + for (auto &cert : client.certs) { + pt::ptree cert_node; + cert_node.put_value(cert); + cert_nodes.push_back(std::make_pair(""s, cert_node)); + } + node.add_child("certs"s, cert_nodes); + + nodes.push_back(std::make_pair(""s, node)); + } + + try { + pt::write_json(config::nvhttp.file_state, root); + } + catch (std::exception &e) { + BOOST_LOG(error) << "Couldn't write "sv << config::nvhttp.file_state << ": "sv << e.what(); return; } } - root.erase("root"s); - - root.put("root.uniqueid", http::unique_id); - auto &nodes = root.add_child("root.devices", pt::ptree {}); - for(auto &[_, client] : map_id_client) { - pt::ptree node; - - node.put("uniqueid"s, client.uniqueID); - - pt::ptree cert_nodes; - for(auto &cert : client.certs) { - pt::ptree cert_node; - cert_node.put_value(cert); - cert_nodes.push_back(std::make_pair(""s, cert_node)); + void + load_state() { + if (!fs::exists(config::nvhttp.file_state)) { + BOOST_LOG(info) << "File "sv << config::nvhttp.file_state << " doesn't exist"sv; + http::unique_id = uuid_util::uuid_t::generate().string(); + return; } - node.add_child("certs"s, cert_nodes); - nodes.push_back(std::make_pair(""s, node)); - } + pt::ptree root; + try { + pt::read_json(config::nvhttp.file_state, root); + } + catch (std::exception &e) { + BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what(); - try { - pt::write_json(config::nvhttp.file_state, root); - } - catch(std::exception &e) { - BOOST_LOG(error) << "Couldn't write "sv << config::nvhttp.file_state << ": "sv << e.what(); - return; - } -} + return; + } -void load_state() { - if(!fs::exists(config::nvhttp.file_state)) { - BOOST_LOG(info) << "File "sv << config::nvhttp.file_state << " doesn't exist"sv; - http::unique_id = uuid_util::uuid_t::generate().string(); - return; - } + auto unique_id_p = root.get_optional("root.uniqueid"); + if (!unique_id_p) { + // This file doesn't contain moonlight credentials + http::unique_id = uuid_util::uuid_t::generate().string(); + return; + } + http::unique_id = std::move(*unique_id_p); - pt::ptree root; - try { - pt::read_json(config::nvhttp.file_state, root); - } - catch(std::exception &e) { - BOOST_LOG(error) << "Couldn't read "sv << config::nvhttp.file_state << ": "sv << e.what(); + auto device_nodes = root.get_child("root.devices"); - return; - } + for (auto &[_, device_node] : device_nodes) { + auto uniqID = device_node.get("uniqueid"); + auto &client = map_id_client.emplace(uniqID, client_t {}).first->second; - auto unique_id_p = root.get_optional("root.uniqueid"); - if(!unique_id_p) { - // This file doesn't contain moonlight credentials - http::unique_id = uuid_util::uuid_t::generate().string(); - return; - } - http::unique_id = std::move(*unique_id_p); + client.uniqueID = uniqID; - auto device_nodes = root.get_child("root.devices"); - - for(auto &[_, device_node] : device_nodes) { - auto uniqID = device_node.get("uniqueid"); - auto &client = map_id_client.emplace(uniqID, client_t {}).first->second; - - client.uniqueID = uniqID; - - for(auto &[_, el] : device_node.get_child("certs")) { - client.certs.emplace_back(el.get_value()); + for (auto &[_, el] : device_node.get_child("certs")) { + client.certs.emplace_back(el.get_value()); + } } } -} -void update_id_client(const std::string &uniqueID, std::string &&cert, op_e op) { - switch(op) { - case op_e::ADD: { - auto &client = map_id_client[uniqueID]; - client.certs.emplace_back(std::move(cert)); - client.uniqueID = uniqueID; - } break; - case op_e::REMOVE: - map_id_client.erase(uniqueID); - break; + void + update_id_client(const std::string &uniqueID, std::string &&cert, op_e op) { + switch (op) { + case op_e::ADD: { + auto &client = map_id_client[uniqueID]; + client.certs.emplace_back(std::move(cert)); + client.uniqueID = uniqueID; + } break; + case op_e::REMOVE: + map_id_client.erase(uniqueID); + break; + } + + if (!config::sunshine.flags[config::flag::FRESH_STATE]) { + save_state(); + } } - if(!config::sunshine.flags[config::flag::FRESH_STATE]) { - save_state(); - } -} + rtsp_stream::launch_session_t + make_launch_session(bool host_audio, const args_t &args) { + rtsp_stream::launch_session_t launch_session; -rtsp_stream::launch_session_t make_launch_session(bool host_audio, const args_t &args) { - rtsp_stream::launch_session_t launch_session; + launch_session.host_audio = host_audio; + launch_session.gcm_key = util::from_hex(get_arg(args, "rikey"), true); + uint32_t prepend_iv = util::endian::big(util::from_view(get_arg(args, "rikeyid"))); + auto prepend_iv_p = (uint8_t *) &prepend_iv; - launch_session.host_audio = host_audio; - launch_session.gcm_key = util::from_hex(get_arg(args, "rikey"), true); - uint32_t prepend_iv = util::endian::big(util::from_view(get_arg(args, "rikeyid"))); - auto prepend_iv_p = (uint8_t *)&prepend_iv; + auto next = std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session.iv)); + std::fill(next, std::end(launch_session.iv), 0); - auto next = std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session.iv)); - std::fill(next, std::end(launch_session.iv), 0); - - return launch_session; -} - -void getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin) { - if(sess.async_insert_pin.salt.size() < 32) { - tree.put("root.paired", 0); - tree.put("root..status_code", 400); - return; + return launch_session; } - std::string_view salt_view { sess.async_insert_pin.salt.data(), 32 }; + void + getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin) { + if (sess.async_insert_pin.salt.size() < 32) { + tree.put("root.paired", 0); + tree.put("root..status_code", 400); + return; + } - auto salt = util::from_hex>(salt_view, true); + std::string_view salt_view { sess.async_insert_pin.salt.data(), 32 }; - auto key = crypto::gen_aes_key(salt, pin); - sess.cipher_key = std::make_unique(key); + auto salt = util::from_hex>(salt_view, true); - tree.put("root.paired", 1); - tree.put("root.plaincert", util::hex_vec(conf_intern.servercert, true)); - tree.put("root..status_code", 200); -} -void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &args) { - auto encrypted_response = util::from_hex_vec(get_arg(args, "serverchallengeresp"), true); + auto key = crypto::gen_aes_key(salt, pin); + sess.cipher_key = std::make_unique(key); - std::vector decrypted; - crypto::cipher::ecb_t cipher(*sess.cipher_key, false); - - cipher.decrypt(encrypted_response, decrypted); - - sess.clienthash = std::move(decrypted); - - auto serversecret = sess.serversecret; - auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret); - - serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign)); - - tree.put("root.pairingsecret", util::hex_vec(serversecret, true)); - tree.put("root.paired", 1); - tree.put("root..status_code", 200); -} - -void clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args) { - auto challenge = util::from_hex_vec(get_arg(args, "clientchallenge"), true); - - crypto::cipher::ecb_t cipher(*sess.cipher_key, false); - - std::vector decrypted; - cipher.decrypt(challenge, decrypted); - - auto x509 = crypto::x509(conf_intern.servercert); - auto sign = crypto::signature(x509); - auto serversecret = crypto::rand(16); - - decrypted.insert(std::end(decrypted), std::begin(sign), std::end(sign)); - decrypted.insert(std::end(decrypted), std::begin(serversecret), std::end(serversecret)); - - auto hash = crypto::hash({ (char *)decrypted.data(), decrypted.size() }); - auto serverchallenge = crypto::rand(16); - - std::string plaintext; - plaintext.reserve(hash.size() + serverchallenge.size()); - - plaintext.insert(std::end(plaintext), std::begin(hash), std::end(hash)); - plaintext.insert(std::end(plaintext), std::begin(serverchallenge), std::end(serverchallenge)); - - std::vector encrypted; - cipher.encrypt(plaintext, encrypted); - - sess.serversecret = std::move(serversecret); - sess.serverchallenge = std::move(serverchallenge); - - tree.put("root.paired", 1); - tree.put("root.challengeresponse", util::hex_vec(encrypted, true)); - tree.put("root..status_code", 200); -} - -void clientpairingsecret(std::shared_ptr> &add_cert, pair_session_t &sess, pt::ptree &tree, const args_t &args) { - auto &client = sess.client; - - auto pairingsecret = util::from_hex_vec(get_arg(args, "clientpairingsecret"), true); - - std::string_view secret { pairingsecret.data(), 16 }; - std::string_view sign { pairingsecret.data() + secret.size(), crypto::digest_size }; - - assert((secret.size() + sign.size()) == pairingsecret.size()); - - auto x509 = crypto::x509(client.cert); - auto x509_sign = crypto::signature(x509); - - std::string data; - data.reserve(sess.serverchallenge.size() + x509_sign.size() + secret.size()); - - data.insert(std::end(data), std::begin(sess.serverchallenge), std::end(sess.serverchallenge)); - data.insert(std::end(data), std::begin(x509_sign), std::end(x509_sign)); - data.insert(std::end(data), std::begin(secret), std::end(secret)); - - auto hash = crypto::hash(data); - - // if hash not correct, probably MITM - if(std::memcmp(hash.data(), sess.clienthash.data(), hash.size())) { - // TODO: log - - map_id_sess.erase(client.uniqueID); - tree.put("root.paired", 0); - } - - if(crypto::verify256(crypto::x509(client.cert), secret, sign)) { tree.put("root.paired", 1); - add_cert->raise(crypto::x509(client.cert)); - - auto it = map_id_sess.find(client.uniqueID); - - update_id_client(client.uniqueID, std::move(client.cert), op_e::ADD); - map_id_sess.erase(it); + tree.put("root.plaincert", util::hex_vec(conf_intern.servercert, true)); + tree.put("root..status_code", 200); } - else { - map_id_sess.erase(client.uniqueID); - tree.put("root.paired", 0); + void + serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &args) { + auto encrypted_response = util::from_hex_vec(get_arg(args, "serverchallengeresp"), true); + + std::vector decrypted; + crypto::cipher::ecb_t cipher(*sess.cipher_key, false); + + cipher.decrypt(encrypted_response, decrypted); + + sess.clienthash = std::move(decrypted); + + auto serversecret = sess.serversecret; + auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret); + + serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign)); + + tree.put("root.pairingsecret", util::hex_vec(serversecret, true)); + tree.put("root.paired", 1); + tree.put("root..status_code", 200); } - tree.put("root..status_code", 200); -} + void + clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args) { + auto challenge = util::from_hex_vec(get_arg(args, "clientchallenge"), true); -template -struct tunnel; + crypto::cipher::ecb_t cipher(*sess.cipher_key, false); -template<> -struct tunnel { - static auto constexpr to_string = "HTTPS"sv; -}; + std::vector decrypted; + cipher.decrypt(challenge, decrypted); -template<> -struct tunnel { - static auto constexpr to_string = "NONE"sv; -}; + auto x509 = crypto::x509(conf_intern.servercert); + auto sign = crypto::signature(x509); + auto serversecret = crypto::rand(16); -template -void print_req(std::shared_ptr::Request> request) { - BOOST_LOG(debug) << "TUNNEL :: "sv << tunnel::to_string; + decrypted.insert(std::end(decrypted), std::begin(sign), std::end(sign)); + decrypted.insert(std::end(decrypted), std::begin(serversecret), std::end(serversecret)); - BOOST_LOG(debug) << "METHOD :: "sv << request->method; - BOOST_LOG(debug) << "DESTINATION :: "sv << request->path; + auto hash = crypto::hash({ (char *) decrypted.data(), decrypted.size() }); + auto serverchallenge = crypto::rand(16); - for(auto &[name, val] : request->header) { - BOOST_LOG(debug) << name << " -- " << val; + std::string plaintext; + plaintext.reserve(hash.size() + serverchallenge.size()); + + plaintext.insert(std::end(plaintext), std::begin(hash), std::end(hash)); + plaintext.insert(std::end(plaintext), std::begin(serverchallenge), std::end(serverchallenge)); + + std::vector encrypted; + cipher.encrypt(plaintext, encrypted); + + sess.serversecret = std::move(serversecret); + sess.serverchallenge = std::move(serverchallenge); + + tree.put("root.paired", 1); + tree.put("root.challengeresponse", util::hex_vec(encrypted, true)); + tree.put("root..status_code", 200); } - BOOST_LOG(debug) << " [--] "sv; + void + clientpairingsecret(std::shared_ptr> &add_cert, pair_session_t &sess, pt::ptree &tree, const args_t &args) { + auto &client = sess.client; - for(auto &[name, val] : request->parse_query_string()) { - BOOST_LOG(debug) << name << " -- " << val; + auto pairingsecret = util::from_hex_vec(get_arg(args, "clientpairingsecret"), true); + + std::string_view secret { pairingsecret.data(), 16 }; + std::string_view sign { pairingsecret.data() + secret.size(), crypto::digest_size }; + + assert((secret.size() + sign.size()) == pairingsecret.size()); + + auto x509 = crypto::x509(client.cert); + auto x509_sign = crypto::signature(x509); + + std::string data; + data.reserve(sess.serverchallenge.size() + x509_sign.size() + secret.size()); + + data.insert(std::end(data), std::begin(sess.serverchallenge), std::end(sess.serverchallenge)); + data.insert(std::end(data), std::begin(x509_sign), std::end(x509_sign)); + data.insert(std::end(data), std::begin(secret), std::end(secret)); + + auto hash = crypto::hash(data); + + // if hash not correct, probably MITM + if (std::memcmp(hash.data(), sess.clienthash.data(), hash.size())) { + // TODO: log + + map_id_sess.erase(client.uniqueID); + tree.put("root.paired", 0); + } + + if (crypto::verify256(crypto::x509(client.cert), secret, sign)) { + tree.put("root.paired", 1); + add_cert->raise(crypto::x509(client.cert)); + + auto it = map_id_sess.find(client.uniqueID); + + update_id_client(client.uniqueID, std::move(client.cert), op_e::ADD); + map_id_sess.erase(it); + } + else { + map_id_sess.erase(client.uniqueID); + tree.put("root.paired", 0); + } + + tree.put("root..status_code", 200); } - BOOST_LOG(debug) << " [--] "sv; -} + template + struct tunnel; -template -void not_found(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { - print_req(request); + template <> + struct tunnel { + static auto constexpr to_string = "HTTPS"sv; + }; - pt::ptree tree; - tree.put("root..status_code", 404); + template <> + struct tunnel { + static auto constexpr to_string = "NONE"sv; + }; - std::ostringstream data; + template + void + print_req(std::shared_ptr::Request> request) { + BOOST_LOG(debug) << "TUNNEL :: "sv << tunnel::to_string; - pt::write_xml(data, tree); - response->write(data.str()); + BOOST_LOG(debug) << "METHOD :: "sv << request->method; + BOOST_LOG(debug) << "DESTINATION :: "sv << request->path; - *response - << "HTTP/1.1 404 NOT FOUND\r\n" - << data.str(); + for (auto &[name, val] : request->header) { + BOOST_LOG(debug) << name << " -- " << val; + } - response->close_connection_after_response = true; -} + BOOST_LOG(debug) << " [--] "sv; -template -void pair(std::shared_ptr> &add_cert, std::shared_ptr::Response> response, std::shared_ptr::Request> request) { - print_req(request); + for (auto &[name, val] : request->parse_query_string()) { + BOOST_LOG(debug) << name << " -- " << val; + } - pt::ptree tree; + BOOST_LOG(debug) << " [--] "sv; + } + + template + void + not_found(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + print_req(request); + + pt::ptree tree; + tree.put("root..status_code", 404); - auto fg = util::fail_guard([&]() { std::ostringstream data; pt::write_xml(data, tree); response->write(data.str()); + + *response + << "HTTP/1.1 404 NOT FOUND\r\n" + << data.str(); + response->close_connection_after_response = true; - }); - - auto args = request->parse_query_string(); - if(args.find("uniqueid"s) == std::end(args)) { - tree.put("root..status_code", 400); - - return; } - auto uniqID { std::move(get_arg(args, "uniqueid")) }; - auto sess_it = map_id_sess.find(uniqID); + template + void + pair(std::shared_ptr> &add_cert, std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + print_req(request); - args_t::const_iterator it; - if(it = args.find("phrase"); it != std::end(args)) { - if(it->second == "getservercert"sv) { - pair_session_t sess; + pt::ptree tree; - sess.client.uniqueID = std::move(uniqID); - sess.client.cert = util::from_hex_vec(get_arg(args, "clientcert"), true); + auto fg = util::fail_guard([&]() { + std::ostringstream data; - BOOST_LOG(debug) << sess.client.cert; - auto ptr = map_id_sess.emplace(sess.client.uniqueID, std::move(sess)).first; + pt::write_xml(data, tree); + response->write(data.str()); + response->close_connection_after_response = true; + }); - ptr->second.async_insert_pin.salt = std::move(get_arg(args, "salt")); + auto args = request->parse_query_string(); + if (args.find("uniqueid"s) == std::end(args)) { + tree.put("root..status_code", 400); - if(config::sunshine.flags[config::flag::PIN_STDIN]) { - std::string pin; + return; + } - std::cout << "Please insert pin: "sv; - std::getline(std::cin, pin); + auto uniqID { std::move(get_arg(args, "uniqueid")) }; + auto sess_it = map_id_sess.find(uniqID); - getservercert(ptr->second, tree, pin); + args_t::const_iterator it; + if (it = args.find("phrase"); it != std::end(args)) { + if (it->second == "getservercert"sv) { + pair_session_t sess; + + sess.client.uniqueID = std::move(uniqID); + sess.client.cert = util::from_hex_vec(get_arg(args, "clientcert"), true); + + BOOST_LOG(debug) << sess.client.cert; + auto ptr = map_id_sess.emplace(sess.client.uniqueID, std::move(sess)).first; + + ptr->second.async_insert_pin.salt = std::move(get_arg(args, "salt")); + + if (config::sunshine.flags[config::flag::PIN_STDIN]) { + std::string pin; + + std::cout << "Please insert pin: "sv; + std::getline(std::cin, pin); + + getservercert(ptr->second, tree, pin); + } + else { + ptr->second.async_insert_pin.response = std::move(response); + + fg.disable(); + return; + } } - else { - ptr->second.async_insert_pin.response = std::move(response); - - fg.disable(); - return; + else if (it->second == "pairchallenge"sv) { + tree.put("root.paired", 1); + tree.put("root..status_code", 200); } } - else if(it->second == "pairchallenge"sv) { - tree.put("root.paired", 1); - tree.put("root..status_code", 200); + else if (it = args.find("clientchallenge"); it != std::end(args)) { + clientchallenge(sess_it->second, tree, args); + } + else if (it = args.find("serverchallengeresp"); it != std::end(args)) { + serverchallengeresp(sess_it->second, tree, args); + } + else if (it = args.find("clientpairingsecret"); it != std::end(args)) { + clientpairingsecret(add_cert, sess_it->second, tree, args); + } + else { + tree.put("root..status_code", 404); } } - else if(it = args.find("clientchallenge"); it != std::end(args)) { - clientchallenge(sess_it->second, tree, args); - } - else if(it = args.find("serverchallengeresp"); it != std::end(args)) { - serverchallengeresp(sess_it->second, tree, args); - } - else if(it = args.find("clientpairingsecret"); it != std::end(args)) { - clientpairingsecret(add_cert, sess_it->second, tree, args); - } - else { - tree.put("root..status_code", 404); - } -} -/** + /** * @brief Compare the user supplied pin to the Moonlight pin. * @param pin The user supplied pin. * @return `true` if the pin is correct, `false` otherwise. @@ -520,340 +534,344 @@ void pair(std::shared_ptr> &add_cert, std::shared_ * bool pin_status = nvhttp::pin("1234"); * ``` */ -bool pin(std::string pin) { - pt::ptree tree; - if(map_id_sess.empty()) { - return false; - } - - auto &sess = std::begin(map_id_sess)->second; - getservercert(sess, tree, pin); - - // response to the request for pin - std::ostringstream data; - pt::write_xml(data, tree); - - auto &async_response = sess.async_insert_pin.response; - if(async_response.has_left() && async_response.left()) { - async_response.left()->write(data.str()); - } - else if(async_response.has_right() && async_response.right()) { - async_response.right()->write(data.str()); - } - else { - return false; - } - - // reset async_response - async_response = std::decay_t(); - // response to the current request - return true; -} - -template -void pin(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { - print_req(request); - - response->close_connection_after_response = true; - - auto address = request->remote_endpoint().address().to_string(); - auto ip_type = net::from_address(address); - if(ip_type > http::origin_pin_allowed) { - BOOST_LOG(info) << "/pin: ["sv << address << "] -- denied"sv; - - response->write(SimpleWeb::StatusCode::client_error_forbidden); - - return; - } - - bool pinResponse = pin(request->path_match[1]); - if(pinResponse) { - response->write(SimpleWeb::StatusCode::success_ok); - } - else { - response->write(SimpleWeb::StatusCode::client_error_im_a_teapot); - } -} - -template -void serverinfo(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { - print_req(request); - - int pair_status = 0; - if constexpr(std::is_same_v) { - auto args = request->parse_query_string(); - auto clientID = args.find("uniqueid"s); - - - if(clientID != std::end(args)) { - if(auto it = map_id_client.find(clientID->second); it != std::end(map_id_client)) { - pair_status = 1; - } - } - } - - auto local_endpoint = request->local_endpoint(); - - pt::ptree tree; - - tree.put("root..status_code", 200); - tree.put("root.hostname", config::nvhttp.sunshine_name); - - tree.put("root.appversion", VERSION); - tree.put("root.GfeVersion", GFE_VERSION); - tree.put("root.uniqueid", http::unique_id); - tree.put("root.HttpsPort", map_port(PORT_HTTPS)); - tree.put("root.ExternalPort", map_port(PORT_HTTP)); - tree.put("root.mac", platf::get_mac_address(local_endpoint.address().to_string())); - tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 1 ? "1869449984" : "0"); - tree.put("root.LocalIP", local_endpoint.address().to_string()); - - if(config::video.hevc_mode == 3) { - tree.put("root.ServerCodecModeSupport", "3843"); - } - else if(config::video.hevc_mode == 2) { - tree.put("root.ServerCodecModeSupport", "259"); - } - else { - tree.put("root.ServerCodecModeSupport", "3"); - } - - if(!config::nvhttp.external_ip.empty()) { - tree.put("root.ExternalIP", config::nvhttp.external_ip); - } - - pt::ptree display_nodes; - for(auto &resolution : config::nvhttp.resolutions) { - auto pred = [](auto ch) { return ch == ' ' || ch == '\t' || ch == 'x'; }; - - auto middle = std::find_if(std::begin(resolution), std::end(resolution), pred); - if(middle == std::end(resolution)) { - BOOST_LOG(warning) << resolution << " is not in the proper format for a resolution: WIDTHxHEIGHT"sv; - continue; + bool + pin(std::string pin) { + pt::ptree tree; + if (map_id_sess.empty()) { + return false; } - auto width = util::from_chars(&*std::begin(resolution), &*middle); - auto height = util::from_chars(&*(middle + 1), &*std::end(resolution)); - for(auto fps : config::nvhttp.fps) { - pt::ptree display_node; - display_node.put("Width", width); - display_node.put("Height", height); - display_node.put("RefreshRate", fps); + auto &sess = std::begin(map_id_sess)->second; + getservercert(sess, tree, pin); - display_nodes.add_child("DisplayMode", display_node); - } - } - - if(!config::nvhttp.resolutions.empty()) { - tree.add_child("root.SupportedDisplayMode", display_nodes); - } - auto current_appid = proc::proc.running(); - tree.put("root.PairStatus", pair_status); - tree.put("root.currentgame", current_appid); - tree.put("root.state", current_appid > 0 ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE"); - - std::ostringstream data; - - pt::write_xml(data, tree); - response->write(data.str()); - response->close_connection_after_response = true; -} - -void applist(resp_https_t response, req_https_t request) { - print_req(request); - - pt::ptree tree; - - auto g = util::fail_guard([&]() { + // response to the request for pin std::ostringstream data; - pt::write_xml(data, tree); - response->write(data.str()); + + auto &async_response = sess.async_insert_pin.response; + if (async_response.has_left() && async_response.left()) { + async_response.left()->write(data.str()); + } + else if (async_response.has_right() && async_response.right()) { + async_response.right()->write(data.str()); + } + else { + return false; + } + + // reset async_response + async_response = std::decay_t(); + // response to the current request + return true; + } + + template + void + pin(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + print_req(request); + response->close_connection_after_response = true; - }); - auto args = request->parse_query_string(); - if(args.find("uniqueid"s) == std::end(args)) { - tree.put("root..status_code", 400); + auto address = request->remote_endpoint().address().to_string(); + auto ip_type = net::from_address(address); + if (ip_type > http::origin_pin_allowed) { + BOOST_LOG(info) << "/pin: ["sv << address << "] -- denied"sv; - return; - } - - auto clientID = get_arg(args, "uniqueid"); - - auto client = map_id_client.find(clientID); - if(client == std::end(map_id_client)) { - tree.put("root..status_code", 501); - - return; - } - - auto &apps = tree.add_child("root", pt::ptree {}); - - apps.put(".status_code", 200); - - for(auto &proc : proc::proc.get_apps()) { - pt::ptree app; - - app.put("IsHdrSupported"s, config::video.hevc_mode == 3 ? 1 : 0); - app.put("AppTitle"s, proc.name); - app.put("ID", proc.id); - - apps.push_back(std::make_pair("App", std::move(app))); - } -} - -void launch(bool &host_audio, resp_https_t response, req_https_t request) { - print_req(request); - - pt::ptree tree; - auto g = util::fail_guard([&]() { - std::ostringstream data; - - pt::write_xml(data, tree); - response->write(data.str()); - response->close_connection_after_response = true; - }); - - if(rtsp_stream::session_count() == config::stream.channels) { - tree.put("root.resume", 0); - tree.put("root..status_code", 503); - - return; - } - - auto args = request->parse_query_string(); - if( - args.find("rikey"s) == std::end(args) || - args.find("rikeyid"s) == std::end(args) || - args.find("localAudioPlayMode"s) == std::end(args) || - args.find("appid"s) == std::end(args)) { - - tree.put("root.resume", 0); - tree.put("root..status_code", 400); - - return; - } - - auto appid = util::from_view(get_arg(args, "appid")); - - auto current_appid = proc::proc.running(); - if(current_appid > 0) { - tree.put("root.resume", 0); - tree.put("root..status_code", 400); - - return; - } - - if(appid > 0) { - auto err = proc::proc.execute(appid); - if(err) { - tree.put("root..status_code", err); - tree.put("root.gamesession", 0); + response->write(SimpleWeb::StatusCode::client_error_forbidden); return; } + + bool pinResponse = pin(request->path_match[1]); + if (pinResponse) { + response->write(SimpleWeb::StatusCode::success_ok); + } + else { + response->write(SimpleWeb::StatusCode::client_error_im_a_teapot); + } } - host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); - rtsp_stream::launch_session_raise(make_launch_session(host_audio, args)); + template + void + serverinfo(std::shared_ptr::Response> response, std::shared_ptr::Request> request) { + print_req(request); - tree.put("root..status_code", 200); - tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint().address().to_string() + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); - tree.put("root.gamesession", 1); -} + int pair_status = 0; + if constexpr (std::is_same_v) { + auto args = request->parse_query_string(); + auto clientID = args.find("uniqueid"s); -void resume(bool &host_audio, resp_https_t response, req_https_t request) { - print_req(request); + if (clientID != std::end(args)) { + if (auto it = map_id_client.find(clientID->second); it != std::end(map_id_client)) { + pair_status = 1; + } + } + } + + auto local_endpoint = request->local_endpoint(); + + pt::ptree tree; + + tree.put("root..status_code", 200); + tree.put("root.hostname", config::nvhttp.sunshine_name); + + tree.put("root.appversion", VERSION); + tree.put("root.GfeVersion", GFE_VERSION); + tree.put("root.uniqueid", http::unique_id); + tree.put("root.HttpsPort", map_port(PORT_HTTPS)); + tree.put("root.ExternalPort", map_port(PORT_HTTP)); + tree.put("root.mac", platf::get_mac_address(local_endpoint.address().to_string())); + tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 1 ? "1869449984" : "0"); + tree.put("root.LocalIP", local_endpoint.address().to_string()); + + if (config::video.hevc_mode == 3) { + tree.put("root.ServerCodecModeSupport", "3843"); + } + else if (config::video.hevc_mode == 2) { + tree.put("root.ServerCodecModeSupport", "259"); + } + else { + tree.put("root.ServerCodecModeSupport", "3"); + } + + if (!config::nvhttp.external_ip.empty()) { + tree.put("root.ExternalIP", config::nvhttp.external_ip); + } + + pt::ptree display_nodes; + for (auto &resolution : config::nvhttp.resolutions) { + auto pred = [](auto ch) { return ch == ' ' || ch == '\t' || ch == 'x'; }; + + auto middle = std::find_if(std::begin(resolution), std::end(resolution), pred); + if (middle == std::end(resolution)) { + BOOST_LOG(warning) << resolution << " is not in the proper format for a resolution: WIDTHxHEIGHT"sv; + continue; + } + + auto width = util::from_chars(&*std::begin(resolution), &*middle); + auto height = util::from_chars(&*(middle + 1), &*std::end(resolution)); + for (auto fps : config::nvhttp.fps) { + pt::ptree display_node; + display_node.put("Width", width); + display_node.put("Height", height); + display_node.put("RefreshRate", fps); + + display_nodes.add_child("DisplayMode", display_node); + } + } + + if (!config::nvhttp.resolutions.empty()) { + tree.add_child("root.SupportedDisplayMode", display_nodes); + } + auto current_appid = proc::proc.running(); + tree.put("root.PairStatus", pair_status); + tree.put("root.currentgame", current_appid); + tree.put("root.state", current_appid > 0 ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE"); - pt::ptree tree; - auto g = util::fail_guard([&]() { std::ostringstream data; pt::write_xml(data, tree); response->write(data.str()); response->close_connection_after_response = true; - }); - - // It is possible that due a race condition that this if-statement gives a false negative, - // that is automatically resolved in rtsp_server_t - if(rtsp_stream::session_count() == config::stream.channels) { - tree.put("root.resume", 0); - tree.put("root..status_code", 503); - - return; } - auto current_appid = proc::proc.running(); - if(current_appid == 0) { - tree.put("root.resume", 0); - tree.put("root..status_code", 503); + void + applist(resp_https_t response, req_https_t request) { + print_req(request); - return; + pt::ptree tree; + + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_xml(data, tree); + response->write(data.str()); + response->close_connection_after_response = true; + }); + + auto args = request->parse_query_string(); + if (args.find("uniqueid"s) == std::end(args)) { + tree.put("root..status_code", 400); + + return; + } + + auto clientID = get_arg(args, "uniqueid"); + + auto client = map_id_client.find(clientID); + if (client == std::end(map_id_client)) { + tree.put("root..status_code", 501); + + return; + } + + auto &apps = tree.add_child("root", pt::ptree {}); + + apps.put(".status_code", 200); + + for (auto &proc : proc::proc.get_apps()) { + pt::ptree app; + + app.put("IsHdrSupported"s, config::video.hevc_mode == 3 ? 1 : 0); + app.put("AppTitle"s, proc.name); + app.put("ID", proc.id); + + apps.push_back(std::make_pair("App", std::move(app))); + } } - auto args = request->parse_query_string(); - if( - args.find("rikey"s) == std::end(args) || - args.find("rikeyid"s) == std::end(args)) { + void + launch(bool &host_audio, resp_https_t response, req_https_t request) { + print_req(request); - tree.put("root.resume", 0); - tree.put("root..status_code", 400); + pt::ptree tree; + auto g = util::fail_guard([&]() { + std::ostringstream data; - return; + pt::write_xml(data, tree); + response->write(data.str()); + response->close_connection_after_response = true; + }); + + if (rtsp_stream::session_count() == config::stream.channels) { + tree.put("root.resume", 0); + tree.put("root..status_code", 503); + + return; + } + + auto args = request->parse_query_string(); + if ( + args.find("rikey"s) == std::end(args) || + args.find("rikeyid"s) == std::end(args) || + args.find("localAudioPlayMode"s) == std::end(args) || + args.find("appid"s) == std::end(args)) { + tree.put("root.resume", 0); + tree.put("root..status_code", 400); + + return; + } + + auto appid = util::from_view(get_arg(args, "appid")); + + auto current_appid = proc::proc.running(); + if (current_appid > 0) { + tree.put("root.resume", 0); + tree.put("root..status_code", 400); + + return; + } + + if (appid > 0) { + auto err = proc::proc.execute(appid); + if (err) { + tree.put("root..status_code", err); + tree.put("root.gamesession", 0); + + return; + } + } + + host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); + rtsp_stream::launch_session_raise(make_launch_session(host_audio, args)); + + tree.put("root..status_code", 200); + tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint().address().to_string() + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); + tree.put("root.gamesession", 1); } - rtsp_stream::launch_session_raise(make_launch_session(host_audio, args)); + void + resume(bool &host_audio, resp_https_t response, req_https_t request) { + print_req(request); - tree.put("root..status_code", 200); - tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint().address().to_string() + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); - tree.put("root.resume", 1); -} + pt::ptree tree; + auto g = util::fail_guard([&]() { + std::ostringstream data; -void cancel(resp_https_t response, req_https_t request) { - print_req(request); + pt::write_xml(data, tree); + response->write(data.str()); + response->close_connection_after_response = true; + }); - pt::ptree tree; - auto g = util::fail_guard([&]() { - std::ostringstream data; + // It is possible that due a race condition that this if-statement gives a false negative, + // that is automatically resolved in rtsp_server_t + if (rtsp_stream::session_count() == config::stream.channels) { + tree.put("root.resume", 0); + tree.put("root..status_code", 503); - pt::write_xml(data, tree); - response->write(data.str()); + return; + } + + auto current_appid = proc::proc.running(); + if (current_appid == 0) { + tree.put("root.resume", 0); + tree.put("root..status_code", 503); + + return; + } + + auto args = request->parse_query_string(); + if ( + args.find("rikey"s) == std::end(args) || + args.find("rikeyid"s) == std::end(args)) { + tree.put("root.resume", 0); + tree.put("root..status_code", 400); + + return; + } + + rtsp_stream::launch_session_raise(make_launch_session(host_audio, args)); + + tree.put("root..status_code", 200); + tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint().address().to_string() + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); + tree.put("root.resume", 1); + } + + void + cancel(resp_https_t response, req_https_t request) { + print_req(request); + + pt::ptree tree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_xml(data, tree); + response->write(data.str()); + response->close_connection_after_response = true; + }); + + // It is possible that due a race condition that this if-statement gives a false positive, + // the client should try again + if (rtsp_stream::session_count() != 0) { + tree.put("root.resume", 0); + tree.put("root..status_code", 503); + + return; + } + + tree.put("root.cancel", 1); + tree.put("root..status_code", 200); + + if (proc::proc.running() > 0) { + proc::proc.terminate(); + } + } + + void + appasset(resp_https_t response, req_https_t request) { + print_req(request); + + auto args = request->parse_query_string(); + auto app_image = proc::proc.get_app_image(util::from_view(get_arg(args, "appid"))); + + std::ifstream in(app_image, std::ios::binary); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "image/png"); + response->write(SimpleWeb::StatusCode::success_ok, in, headers); response->close_connection_after_response = true; - }); - - // It is possible that due a race condition that this if-statement gives a false positive, - // the client should try again - if(rtsp_stream::session_count() != 0) { - tree.put("root.resume", 0); - tree.put("root..status_code", 503); - - return; } - tree.put("root.cancel", 1); - tree.put("root..status_code", 200); - - if(proc::proc.running() > 0) { - proc::proc.terminate(); - } -} - - -void appasset(resp_https_t response, req_https_t request) { - print_req(request); - - auto args = request->parse_query_string(); - auto app_image = proc::proc.get_app_image(util::from_view(get_arg(args, "appid"))); - - std::ifstream in(app_image, std::ios::binary); - SimpleWeb::CaseInsensitiveMultimap headers; - headers.emplace("Content-Type", "image/png"); - response->write(SimpleWeb::StatusCode::success_ok, in, headers); - response->close_connection_after_response = true; -} - -/** + /** * @brief Start the nvhttp server. * * EXAMPLES: @@ -861,145 +879,145 @@ void appasset(resp_https_t response, req_https_t request) { * nvhttp::start(); * ``` */ -void start() { - auto shutdown_event = mail::man->event(mail::shutdown); + void + start() { + auto shutdown_event = mail::man->event(mail::shutdown); - auto port_http = map_port(PORT_HTTP); - auto port_https = map_port(PORT_HTTPS); + auto port_http = map_port(PORT_HTTP); + auto port_https = map_port(PORT_HTTPS); - bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE]; + bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE]; - if(!clean_slate) { - load_state(); - } - - conf_intern.pkey = read_file(config::nvhttp.pkey.c_str()); - conf_intern.servercert = read_file(config::nvhttp.cert.c_str()); - - crypto::cert_chain_t cert_chain; - for(auto &[_, client] : map_id_client) { - for(auto &cert : client.certs) { - cert_chain.add(crypto::x509(cert)); - } - } - - auto add_cert = std::make_shared>(30); - - // /resume doesn't get the parameter "localAudioPlayMode" - // /launch will store it in host_audio - bool host_audio {}; - - https_server_t https_server { config::nvhttp.cert, config::nvhttp.pkey }; - http_server_t http_server; - - // Verify certificates after establishing connection - https_server.verify = [&cert_chain, add_cert](SSL *ssl) { - auto x509 = SSL_get_peer_certificate(ssl); - if(!x509) { - BOOST_LOG(info) << "unknown -- denied"sv; - return 0; + if (!clean_slate) { + load_state(); } - int verified = 0; + conf_intern.pkey = read_file(config::nvhttp.pkey.c_str()); + conf_intern.servercert = read_file(config::nvhttp.cert.c_str()); - auto fg = util::fail_guard([&]() { - char subject_name[256]; - - - X509_NAME_oneline(X509_get_subject_name(x509), subject_name, sizeof(subject_name)); - - BOOST_LOG(debug) << subject_name << " -- "sv << (verified ? "verified"sv : "denied"sv); - }); - - while(add_cert->peek()) { - char subject_name[256]; - - auto cert = add_cert->pop(); - X509_NAME_oneline(X509_get_subject_name(cert.get()), subject_name, sizeof(subject_name)); - - BOOST_LOG(debug) << "Added cert ["sv << subject_name << ']'; - cert_chain.add(std::move(cert)); + crypto::cert_chain_t cert_chain; + for (auto &[_, client] : map_id_client) { + for (auto &cert : client.certs) { + cert_chain.add(crypto::x509(cert)); + } } - auto err_str = cert_chain.verify(x509); - if(err_str) { - BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str; + auto add_cert = std::make_shared>(30); - return verified; - } + // /resume doesn't get the parameter "localAudioPlayMode" + // /launch will store it in host_audio + bool host_audio {}; - verified = 1; + https_server_t https_server { config::nvhttp.cert, config::nvhttp.pkey }; + http_server_t http_server; - return verified; - }; - - https_server.on_verify_failed = [](resp_https_t resp, req_https_t req) { - pt::ptree tree; - auto g = util::fail_guard([&]() { - std::ostringstream data; - - pt::write_xml(data, tree); - resp->write(data.str()); - resp->close_connection_after_response = true; - }); - - tree.put("root..status_code"s, 401); - tree.put("root..query"s, req->path); - tree.put("root..status_message"s, "The client is not authorized. Certificate verification failed."s); - }; - - https_server.default_resource["GET"] = not_found; - https_server.resource["^/serverinfo$"]["GET"] = serverinfo; - https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair(add_cert, resp, req); }; - https_server.resource["^/applist$"]["GET"] = applist; - https_server.resource["^/appasset$"]["GET"] = appasset; - https_server.resource["^/launch$"]["GET"] = [&host_audio](auto resp, auto req) { launch(host_audio, resp, req); }; - https_server.resource["^/pin/([0-9]+)$"]["GET"] = pin; - https_server.resource["^/resume$"]["GET"] = [&host_audio](auto resp, auto req) { resume(host_audio, resp, req); }; - https_server.resource["^/cancel$"]["GET"] = cancel; - - https_server.config.reuse_address = true; - https_server.config.address = "0.0.0.0"s; - https_server.config.port = port_https; - - http_server.default_resource["GET"] = not_found; - http_server.resource["^/serverinfo$"]["GET"] = serverinfo; - http_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair(add_cert, resp, req); }; - http_server.resource["^/pin/([0-9]+)$"]["GET"] = pin; - - http_server.config.reuse_address = true; - http_server.config.address = "0.0.0.0"s; - http_server.config.port = port_http; - - auto accept_and_run = [&](auto *http_server) { - try { - http_server->start(); - } - catch(boost::system::system_error &err) { - // It's possible the exception gets thrown after calling http_server->stop() from a different thread - if(shutdown_event->peek()) { - return; + // Verify certificates after establishing connection + https_server.verify = [&cert_chain, add_cert](SSL *ssl) { + auto x509 = SSL_get_peer_certificate(ssl); + if (!x509) { + BOOST_LOG(info) << "unknown -- denied"sv; + return 0; } - BOOST_LOG(fatal) << "Couldn't start http server on ports ["sv << port_https << ", "sv << port_https << "]: "sv << err.what(); - shutdown_event->raise(true); - return; - } - }; - std::thread ssl { accept_and_run, &https_server }; - std::thread tcp { accept_and_run, &http_server }; + int verified = 0; - // Wait for any event - shutdown_event->view(); + auto fg = util::fail_guard([&]() { + char subject_name[256]; - https_server.stop(); - http_server.stop(); + X509_NAME_oneline(X509_get_subject_name(x509), subject_name, sizeof(subject_name)); - ssl.join(); - tcp.join(); -} + BOOST_LOG(debug) << subject_name << " -- "sv << (verified ? "verified"sv : "denied"sv); + }); -/** + while (add_cert->peek()) { + char subject_name[256]; + + auto cert = add_cert->pop(); + X509_NAME_oneline(X509_get_subject_name(cert.get()), subject_name, sizeof(subject_name)); + + BOOST_LOG(debug) << "Added cert ["sv << subject_name << ']'; + cert_chain.add(std::move(cert)); + } + + auto err_str = cert_chain.verify(x509); + if (err_str) { + BOOST_LOG(warning) << "SSL Verification error :: "sv << err_str; + + return verified; + } + + verified = 1; + + return verified; + }; + + https_server.on_verify_failed = [](resp_https_t resp, req_https_t req) { + pt::ptree tree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_xml(data, tree); + resp->write(data.str()); + resp->close_connection_after_response = true; + }); + + tree.put("root..status_code"s, 401); + tree.put("root..query"s, req->path); + tree.put("root..status_message"s, "The client is not authorized. Certificate verification failed."s); + }; + + https_server.default_resource["GET"] = not_found; + https_server.resource["^/serverinfo$"]["GET"] = serverinfo; + https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair(add_cert, resp, req); }; + https_server.resource["^/applist$"]["GET"] = applist; + https_server.resource["^/appasset$"]["GET"] = appasset; + https_server.resource["^/launch$"]["GET"] = [&host_audio](auto resp, auto req) { launch(host_audio, resp, req); }; + https_server.resource["^/pin/([0-9]+)$"]["GET"] = pin; + https_server.resource["^/resume$"]["GET"] = [&host_audio](auto resp, auto req) { resume(host_audio, resp, req); }; + https_server.resource["^/cancel$"]["GET"] = cancel; + + https_server.config.reuse_address = true; + https_server.config.address = "0.0.0.0"s; + https_server.config.port = port_https; + + http_server.default_resource["GET"] = not_found; + http_server.resource["^/serverinfo$"]["GET"] = serverinfo; + http_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair(add_cert, resp, req); }; + http_server.resource["^/pin/([0-9]+)$"]["GET"] = pin; + + http_server.config.reuse_address = true; + http_server.config.address = "0.0.0.0"s; + http_server.config.port = port_http; + + auto accept_and_run = [&](auto *http_server) { + try { + http_server->start(); + } + catch (boost::system::system_error &err) { + // It's possible the exception gets thrown after calling http_server->stop() from a different thread + if (shutdown_event->peek()) { + return; + } + + BOOST_LOG(fatal) << "Couldn't start http server on ports ["sv << port_https << ", "sv << port_https << "]: "sv << err.what(); + shutdown_event->raise(true); + return; + } + }; + std::thread ssl { accept_and_run, &https_server }; + std::thread tcp { accept_and_run, &http_server }; + + // Wait for any event + shutdown_event->view(); + + https_server.stop(); + http_server.stop(); + + ssl.join(); + tcp.join(); + } + + /** * @brief Remove all paired clients. * * EXAMPLES: @@ -1007,8 +1025,9 @@ void start() { * nvhttp::erase_all_clients(); * ``` */ -void erase_all_clients() { - map_id_client.clear(); - save_state(); -} -} // namespace nvhttp + void + erase_all_clients() { + map_id_client.clear(); + save_state(); + } +} // namespace nvhttp diff --git a/src/nvhttp.h b/src/nvhttp.h index 0639ee57..eccf9d34 100644 --- a/src/nvhttp.h +++ b/src/nvhttp.h @@ -17,31 +17,34 @@ */ namespace nvhttp { -/** + /** * @brief The protocol version. */ -constexpr auto VERSION = "7.1.431.-1"; -// The negative 4th version number tells Moonlight that this is Sunshine + constexpr auto VERSION = "7.1.431.-1"; + // The negative 4th version number tells Moonlight that this is Sunshine -/** + /** * @brief The GFE version we are replicating. */ -constexpr auto GFE_VERSION = "3.23.0.74"; + constexpr auto GFE_VERSION = "3.23.0.74"; -/** + /** * @brief The HTTP port, as a difference from the config port. */ -constexpr auto PORT_HTTP = 0; + constexpr auto PORT_HTTP = 0; -/** + /** * @brief The HTTPS port, as a difference from the config port. */ -constexpr auto PORT_HTTPS = -5; + constexpr auto PORT_HTTPS = -5; -// functions -void start(); -bool pin(std::string pin); -void erase_all_clients(); -} // namespace nvhttp + // functions + void + start(); + bool + pin(std::string pin); + void + erase_all_clients(); +} // namespace nvhttp -#endif // SUNSHINE_NVHTTP_H +#endif // SUNSHINE_NVHTTP_H diff --git a/src/platform/common.h b/src/platform/common.h index f7d96f0e..1175f889 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -27,222 +27,230 @@ struct AVHWFramesContext; // Forward declarations of boost classes to avoid having to include boost headers // here, which results in issues with Windows.h and WinSock2.h include order. namespace boost { -namespace asio { -namespace ip { -class address; -} // namespace ip -} // namespace asio -namespace filesystem { -class path; -} -namespace process { -class child; -class group; -template -class basic_environment; -typedef basic_environment environment; -} // namespace process -} // namespace boost + namespace asio { + namespace ip { + class address; + } // namespace ip + } // namespace asio + namespace filesystem { + class path; + } + namespace process { + class child; + class group; + template + class basic_environment; + typedef basic_environment environment; + } // namespace process +} // namespace boost namespace video { -struct config_t; -} // namespace video + struct config_t; +} // namespace video namespace platf { -constexpr auto MAX_GAMEPADS = 32; + constexpr auto MAX_GAMEPADS = 32; -constexpr std::uint16_t DPAD_UP = 0x0001; -constexpr std::uint16_t DPAD_DOWN = 0x0002; -constexpr std::uint16_t DPAD_LEFT = 0x0004; -constexpr std::uint16_t DPAD_RIGHT = 0x0008; -constexpr std::uint16_t START = 0x0010; -constexpr std::uint16_t BACK = 0x0020; -constexpr std::uint16_t LEFT_STICK = 0x0040; -constexpr std::uint16_t RIGHT_STICK = 0x0080; -constexpr std::uint16_t LEFT_BUTTON = 0x0100; -constexpr std::uint16_t RIGHT_BUTTON = 0x0200; -constexpr std::uint16_t HOME = 0x0400; -constexpr std::uint16_t A = 0x1000; -constexpr std::uint16_t B = 0x2000; -constexpr std::uint16_t X = 0x4000; -constexpr std::uint16_t Y = 0x8000; + constexpr std::uint16_t DPAD_UP = 0x0001; + constexpr std::uint16_t DPAD_DOWN = 0x0002; + constexpr std::uint16_t DPAD_LEFT = 0x0004; + constexpr std::uint16_t DPAD_RIGHT = 0x0008; + constexpr std::uint16_t START = 0x0010; + constexpr std::uint16_t BACK = 0x0020; + constexpr std::uint16_t LEFT_STICK = 0x0040; + constexpr std::uint16_t RIGHT_STICK = 0x0080; + constexpr std::uint16_t LEFT_BUTTON = 0x0100; + constexpr std::uint16_t RIGHT_BUTTON = 0x0200; + constexpr std::uint16_t HOME = 0x0400; + constexpr std::uint16_t A = 0x1000; + constexpr std::uint16_t B = 0x2000; + constexpr std::uint16_t X = 0x4000; + constexpr std::uint16_t Y = 0x8000; -struct rumble_t { - KITTY_DEFAULT_CONSTR(rumble_t) + struct rumble_t { + KITTY_DEFAULT_CONSTR(rumble_t) - rumble_t(std::uint16_t id, std::uint16_t lowfreq, std::uint16_t highfreq) - : id { id }, lowfreq { lowfreq }, highfreq { highfreq } {} + rumble_t(std::uint16_t id, std::uint16_t lowfreq, std::uint16_t highfreq): + id { id }, lowfreq { lowfreq }, highfreq { highfreq } {} - std::uint16_t id; - std::uint16_t lowfreq; - std::uint16_t highfreq; -}; -using rumble_queue_t = safe::mail_raw_t::queue_t; + std::uint16_t id; + std::uint16_t lowfreq; + std::uint16_t highfreq; + }; + using rumble_queue_t = safe::mail_raw_t::queue_t; -namespace speaker { -enum speaker_e { - FRONT_LEFT, - FRONT_RIGHT, - FRONT_CENTER, - LOW_FREQUENCY, - BACK_LEFT, - BACK_RIGHT, - SIDE_LEFT, - SIDE_RIGHT, - MAX_SPEAKERS, -}; + namespace speaker { + enum speaker_e { + FRONT_LEFT, + FRONT_RIGHT, + FRONT_CENTER, + LOW_FREQUENCY, + BACK_LEFT, + BACK_RIGHT, + SIDE_LEFT, + SIDE_RIGHT, + MAX_SPEAKERS, + }; -constexpr std::uint8_t map_stereo[] { - FRONT_LEFT, FRONT_RIGHT -}; -constexpr std::uint8_t map_surround51[] { - FRONT_LEFT, - FRONT_RIGHT, - FRONT_CENTER, - LOW_FREQUENCY, - BACK_LEFT, - BACK_RIGHT, -}; -constexpr std::uint8_t map_surround71[] { - FRONT_LEFT, - FRONT_RIGHT, - FRONT_CENTER, - LOW_FREQUENCY, - BACK_LEFT, - BACK_RIGHT, - SIDE_LEFT, - SIDE_RIGHT, -}; -} // namespace speaker + constexpr std::uint8_t map_stereo[] { + FRONT_LEFT, FRONT_RIGHT + }; + constexpr std::uint8_t map_surround51[] { + FRONT_LEFT, + FRONT_RIGHT, + FRONT_CENTER, + LOW_FREQUENCY, + BACK_LEFT, + BACK_RIGHT, + }; + constexpr std::uint8_t map_surround71[] { + FRONT_LEFT, + FRONT_RIGHT, + FRONT_CENTER, + LOW_FREQUENCY, + BACK_LEFT, + BACK_RIGHT, + SIDE_LEFT, + SIDE_RIGHT, + }; + } // namespace speaker -enum class mem_type_e { - system, - vaapi, - dxgi, - cuda, - unknown -}; + enum class mem_type_e { + system, + vaapi, + dxgi, + cuda, + unknown + }; -enum class pix_fmt_e { - yuv420p, - yuv420p10, - nv12, - p010, - unknown -}; + enum class pix_fmt_e { + yuv420p, + yuv420p10, + nv12, + p010, + unknown + }; -inline std::string_view from_pix_fmt(pix_fmt_e pix_fmt) { - using namespace std::literals; + inline std::string_view + from_pix_fmt(pix_fmt_e pix_fmt) { + using namespace std::literals; #define _CONVERT(x) \ case pix_fmt_e::x: \ return #x##sv - switch(pix_fmt) { - _CONVERT(yuv420p); - _CONVERT(yuv420p10); - _CONVERT(nv12); - _CONVERT(p010); - _CONVERT(unknown); - } + switch (pix_fmt) { + _CONVERT(yuv420p); + _CONVERT(yuv420p10); + _CONVERT(nv12); + _CONVERT(p010); + _CONVERT(unknown); + } #undef _CONVERT - return "unknown"sv; -} - -// Dimensions for touchscreen input -struct touch_port_t { - int offset_x, offset_y; - int width, height; -}; - -struct gamepad_state_t { - std::uint16_t buttonFlags; - std::uint8_t lt; - std::uint8_t rt; - std::int16_t lsX; - std::int16_t lsY; - std::int16_t rsX; - std::int16_t rsY; -}; - -class deinit_t { -public: - virtual ~deinit_t() = default; -}; - -struct img_t { -public: - img_t() = default; - - img_t(img_t &&) = delete; - img_t(const img_t &) = delete; - img_t &operator=(img_t &&) = delete; - img_t &operator=(const img_t &) = delete; - - std::uint8_t *data {}; - std::int32_t width {}; - std::int32_t height {}; - std::int32_t pixel_pitch {}; - std::int32_t row_pitch {}; - - virtual ~img_t() = default; -}; - -struct sink_t { - // Play on host PC - std::string host; - - // On macOS and Windows, it is not possible to create a virtual sink - // Therefore, it is optional - struct null_t { - std::string stereo; - std::string surround51; - std::string surround71; - }; - std::optional null; -}; - -struct hwdevice_t { - void *data {}; - AVFrame *frame {}; - - virtual int convert(platf::img_t &img) { - return -1; + return "unknown"sv; } - /** + // Dimensions for touchscreen input + struct touch_port_t { + int offset_x, offset_y; + int width, height; + }; + + struct gamepad_state_t { + std::uint16_t buttonFlags; + std::uint8_t lt; + std::uint8_t rt; + std::int16_t lsX; + std::int16_t lsY; + std::int16_t rsX; + std::int16_t rsY; + }; + + class deinit_t { + public: + virtual ~deinit_t() = default; + }; + + struct img_t { + public: + img_t() = default; + + img_t(img_t &&) = delete; + img_t(const img_t &) = delete; + img_t & + operator=(img_t &&) = delete; + img_t & + operator=(const img_t &) = delete; + + std::uint8_t *data {}; + std::int32_t width {}; + std::int32_t height {}; + std::int32_t pixel_pitch {}; + std::int32_t row_pitch {}; + + virtual ~img_t() = default; + }; + + struct sink_t { + // Play on host PC + std::string host; + + // On macOS and Windows, it is not possible to create a virtual sink + // Therefore, it is optional + struct null_t { + std::string stereo; + std::string surround51; + std::string surround71; + }; + std::optional null; + }; + + struct hwdevice_t { + void *data {}; + AVFrame *frame {}; + + virtual int + convert(platf::img_t &img) { + return -1; + } + + /** * implementations must take ownership of 'frame' */ - virtual int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { - BOOST_LOG(error) << "Illegal call to hwdevice_t::set_frame(). Did you forget to override it?"; - return -1; - }; + virtual int + set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { + BOOST_LOG(error) << "Illegal call to hwdevice_t::set_frame(). Did you forget to override it?"; + return -1; + }; - virtual void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) {}; + virtual void + set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) {}; - /** + /** * Implementations may set parameters during initialization of the hwframes context */ - virtual void init_hwframes(AVHWFramesContext *frames) {}; + virtual void + init_hwframes(AVHWFramesContext *frames) {}; - /** + /** * Implementations may make modifications required before context derivation */ - virtual int prepare_to_derive_context(int hw_device_type) { - return 0; + virtual int + prepare_to_derive_context(int hw_device_type) { + return 0; + }; + + virtual ~hwdevice_t() = default; }; - virtual ~hwdevice_t() = default; -}; + enum class capture_e : int { + ok, + reinit, + timeout, + error + }; -enum class capture_e : int { - ok, - reinit, - timeout, - error -}; - -class display_t { -public: - /** + class display_t { + public: + /** * When display has a new image ready or a timeout occurs, this callback will be called with the image. * If a frame was captured, frame_captured will be true. If a timeout occurred, it will be false. * @@ -253,11 +261,12 @@ public: * Returns the image object that should be filled next. * This may or may not be the image send with the callback */ - using snapshot_cb_t = std::function(std::shared_ptr &img, bool frame_captured)>; + using snapshot_cb_t = std::function(std::shared_ptr &img, bool frame_captured)>; - display_t() noexcept : offset_x { 0 }, offset_y { 0 } {} + display_t() noexcept: + offset_x { 0 }, offset_y { 0 } {} - /** + /** * snapshot_cb --> the callback * std::shared_ptr img --> The first image to use * bool *cursor --> A pointer to the flag that indicates wether the cursor should be captured as well @@ -267,66 +276,82 @@ public: * capture_e::error on error * capture_e::reinit when need of reinitialization */ - virtual capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) = 0; + virtual capture_e + capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) = 0; - virtual std::shared_ptr alloc_img() = 0; + virtual std::shared_ptr + alloc_img() = 0; - virtual int dummy_img(img_t *img) = 0; + virtual int + dummy_img(img_t *img) = 0; - virtual std::shared_ptr make_hwdevice(pix_fmt_e pix_fmt) { - return std::make_shared(); - } + virtual std::shared_ptr + make_hwdevice(pix_fmt_e pix_fmt) { + return std::make_shared(); + } - virtual bool is_hdr() { - return false; - } + virtual bool + is_hdr() { + return false; + } - virtual bool get_hdr_metadata(SS_HDR_METADATA &metadata) { - std::memset(&metadata, 0, sizeof(metadata)); - return false; - } + virtual bool + get_hdr_metadata(SS_HDR_METADATA &metadata) { + std::memset(&metadata, 0, sizeof(metadata)); + return false; + } - virtual ~display_t() = default; + virtual ~display_t() = default; - // Offsets for when streaming a specific monitor. By default, they are 0. - int offset_x, offset_y; - int env_width, env_height; + // Offsets for when streaming a specific monitor. By default, they are 0. + int offset_x, offset_y; + int env_width, env_height; - int width, height; -}; + int width, height; + }; -class mic_t { -public: - virtual capture_e sample(std::vector &frame_buffer) = 0; + class mic_t { + public: + virtual capture_e + sample(std::vector &frame_buffer) = 0; - virtual ~mic_t() = default; -}; + virtual ~mic_t() = default; + }; -class audio_control_t { -public: - virtual int set_sink(const std::string &sink) = 0; + class audio_control_t { + public: + virtual int + set_sink(const std::string &sink) = 0; - virtual std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) = 0; + virtual std::unique_ptr + microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) = 0; - virtual std::optional sink_info() = 0; + virtual std::optional + sink_info() = 0; - virtual ~audio_control_t() = default; -}; + virtual ~audio_control_t() = default; + }; -void freeInput(void *); + void + freeInput(void *); -using input_t = util::safe_ptr; + using input_t = util::safe_ptr; -std::filesystem::path appdata(); + std::filesystem::path + appdata(); -std::string get_mac_address(const std::string_view &address); + std::string + get_mac_address(const std::string_view &address); -std::string from_sockaddr(const sockaddr *const); -std::pair from_sockaddr_ex(const sockaddr *const); + std::string + from_sockaddr(const sockaddr *const); + std::pair + from_sockaddr_ex(const sockaddr *const); -std::unique_ptr audio_control(); + std::unique_ptr + audio_control(); -/** + /** * display_name --> The name of the monitor that SHOULD be displayed * If display_name is empty --> Use the first monitor that's compatible you can find * If you require to use this parameter in a seperate thread --> make a copy of it. @@ -335,68 +360,92 @@ std::unique_ptr audio_control(); * * Returns display_t based on hwdevice_type */ -std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); + std::shared_ptr + display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); -// A list of names of displays accepted as display_name with the mem_type_e -std::vector display_names(mem_type_e hwdevice_type); + // A list of names of displays accepted as display_name with the mem_type_e + std::vector + display_names(mem_type_e hwdevice_type); -boost::process::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group); + boost::process::child + run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group); -enum class thread_priority_e : int { - low, - normal, - high, - critical -}; -void adjust_thread_priority(thread_priority_e priority); + enum class thread_priority_e : int { + low, + normal, + high, + critical + }; + void + adjust_thread_priority(thread_priority_e priority); -// Allow OS-specific actions to be taken to prepare for streaming -void streaming_will_start(); -void streaming_will_stop(); + // Allow OS-specific actions to be taken to prepare for streaming + void + streaming_will_start(); + void + streaming_will_stop(); -bool restart_supported(); -bool restart(); + bool + restart_supported(); + bool + restart(); -struct batched_send_info_t { - const char *buffer; - size_t block_size; - size_t block_count; + struct batched_send_info_t { + const char *buffer; + size_t block_size; + size_t block_count; - std::uintptr_t native_socket; - boost::asio::ip::address &target_address; - uint16_t target_port; -}; -bool send_batch(batched_send_info_t &send_info); + std::uintptr_t native_socket; + boost::asio::ip::address &target_address; + uint16_t target_port; + }; + bool + send_batch(batched_send_info_t &send_info); -enum class qos_data_type_e : int { - audio, - video -}; -std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type); + enum class qos_data_type_e : int { + audio, + video + }; + std::unique_ptr + enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type); -input_t input(); -void move_mouse(input_t &input, int deltaX, int deltaY); -void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y); -void button_mouse(input_t &input, int button, bool release); -void scroll(input_t &input, int distance); -void hscroll(input_t &input, int distance); -void keyboard(input_t &input, uint16_t modcode, bool release); -void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state); -void unicode(input_t &input, char *utf8, int size); + input_t + input(); + void + move_mouse(input_t &input, int deltaX, int deltaY); + void + abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y); + void + button_mouse(input_t &input, int button, bool release); + void + scroll(input_t &input, int distance); + void + hscroll(input_t &input, int distance); + void + keyboard(input_t &input, uint16_t modcode, bool release); + void + gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state); + void + unicode(input_t &input, char *utf8, int size); -int alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue); -void free_gamepad(input_t &input, int nr); + int + alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue); + void + free_gamepad(input_t &input, int nr); #define SERVICE_NAME "Sunshine" #define SERVICE_TYPE "_nvstream._tcp" -namespace publish { -[[nodiscard]] std::unique_ptr start(); -} + namespace publish { + [[nodiscard]] std::unique_ptr + start(); + } -[[nodiscard]] std::unique_ptr init(); + [[nodiscard]] std::unique_ptr + init(); -std::vector &supported_gamepads(); -} // namespace platf + std::vector & + supported_gamepads(); +} // namespace platf -#endif //SUNSHINE_COMMON_H +#endif //SUNSHINE_COMMON_H diff --git a/src/platform/linux/audio.cpp b/src/platform/linux/audio.cpp index 7973a384..0b68fc01 100644 --- a/src/platform/linux/audio.cpp +++ b/src/platform/linux/audio.cpp @@ -17,491 +17,509 @@ #include "src/thread_safe.h" namespace platf { -using namespace std::literals; + using namespace std::literals; -constexpr pa_channel_position_t position_mapping[] { - PA_CHANNEL_POSITION_FRONT_LEFT, - PA_CHANNEL_POSITION_FRONT_RIGHT, - PA_CHANNEL_POSITION_FRONT_CENTER, - PA_CHANNEL_POSITION_LFE, - PA_CHANNEL_POSITION_REAR_LEFT, - PA_CHANNEL_POSITION_REAR_RIGHT, - PA_CHANNEL_POSITION_SIDE_LEFT, - PA_CHANNEL_POSITION_SIDE_RIGHT, -}; - -std::string to_string(const char *name, const std::uint8_t *mapping, int channels) { - std::stringstream ss; - - ss << "rate=48000 sink_name="sv << name << " format=s16le channels="sv << channels << " channel_map="sv; - std::for_each_n(mapping, channels - 1, [&ss](std::uint8_t pos) { - ss << pa_channel_position_to_string(position_mapping[pos]) << ','; - }); - - ss << pa_channel_position_to_string(position_mapping[mapping[channels - 1]]); - - ss << " sink_properties=device.description="sv << name; - auto result = ss.str(); - - BOOST_LOG(debug) << "null-sink args: "sv << result; - return result; -} - -struct mic_attr_t : public mic_t { - util::safe_ptr mic; - - capture_e sample(std::vector &sample_buf) override { - auto sample_size = sample_buf.size(); - - auto buf = sample_buf.data(); - int status; - if(pa_simple_read(mic.get(), buf, sample_size * 2, &status)) { - BOOST_LOG(error) << "pa_simple_read() failed: "sv << pa_strerror(status); - - return capture_e::error; - } - - return capture_e::ok; - } -}; - -std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, std::string source_name) { - auto mic = std::make_unique(); - - pa_sample_spec ss { PA_SAMPLE_S16LE, sample_rate, (std::uint8_t)channels }; - pa_channel_map pa_map; - - pa_map.channels = channels; - std::for_each_n(pa_map.map, pa_map.channels, [mapping](auto &channel) mutable { - channel = position_mapping[*mapping++]; - }); - - pa_buffer_attr pa_attr = {}; - pa_attr.maxlength = frame_size * 8; - - int status; - - mic->mic.reset( - pa_simple_new(nullptr, "sunshine", - pa_stream_direction_t::PA_STREAM_RECORD, source_name.c_str(), - "sunshine-record", &ss, &pa_map, &pa_attr, &status)); - - if(!mic->mic) { - auto err_str = pa_strerror(status); - BOOST_LOG(error) << "pa_simple_new() failed: "sv << err_str; - return nullptr; - } - - return mic; -} - -namespace pa { -template -struct add_const_helper; - -template -struct add_const_helper { - using type = const std::remove_pointer_t *; -}; - -template -struct add_const_helper { - using type = const T *; -}; - -template -using add_const_t = typename add_const_helper, T>::type; - -template -void pa_free(T *p) { - pa_xfree(p); -} -using ctx_t = util::safe_ptr; -using loop_t = util::safe_ptr; -using op_t = util::safe_ptr; -using string_t = util::safe_ptr>; - -template -using cb_simple_t = std::function i)>; - -template -void cb(ctx_t::pointer ctx, add_const_t i, void *userdata) { - auto &f = *(cb_simple_t *)userdata; - - // Cannot similarly filter on eol here. Unless reported otherwise assume - // we have no need for special filtering like cb? - f(ctx, i); -} - -template -using cb_t = std::function i, int eol)>; - -template -void cb(ctx_t::pointer ctx, add_const_t i, int eol, void *userdata) { - auto &f = *(cb_t *)userdata; - - // For some reason, pulseaudio calls this callback after disconnecting - if(i && eol) { - return; - } - - f(ctx, i, eol); -} - -void cb_i(ctx_t::pointer ctx, std::uint32_t i, void *userdata) { - auto alarm = (safe::alarm_raw_t *)userdata; - - alarm->ring(i); -} - -void ctx_state_cb(ctx_t::pointer ctx, void *userdata) { - auto &f = *(std::function *)userdata; - - f(ctx); -} - -void success_cb(ctx_t::pointer ctx, int status, void *userdata) { - assert(userdata != nullptr); - - auto alarm = (safe::alarm_raw_t *)userdata; - alarm->ring(status ? 0 : 1); -} - -class server_t : public audio_control_t { - enum ctx_event_e : int { - ready, - terminated, - failed + constexpr pa_channel_position_t position_mapping[] { + PA_CHANNEL_POSITION_FRONT_LEFT, + PA_CHANNEL_POSITION_FRONT_RIGHT, + PA_CHANNEL_POSITION_FRONT_CENTER, + PA_CHANNEL_POSITION_LFE, + PA_CHANNEL_POSITION_REAR_LEFT, + PA_CHANNEL_POSITION_REAR_RIGHT, + PA_CHANNEL_POSITION_SIDE_LEFT, + PA_CHANNEL_POSITION_SIDE_RIGHT, }; -public: - loop_t loop; - ctx_t ctx; - std::string requested_sink; + std::string + to_string(const char *name, const std::uint8_t *mapping, int channels) { + std::stringstream ss; - struct { - std::uint32_t stereo = PA_INVALID_INDEX; - std::uint32_t surround51 = PA_INVALID_INDEX; - std::uint32_t surround71 = PA_INVALID_INDEX; - } index; - - std::unique_ptr> events; - std::unique_ptr> events_cb; - - std::thread worker; - int init() { - events = std::make_unique>(); - loop.reset(pa_mainloop_new()); - ctx.reset(pa_context_new(pa_mainloop_get_api(loop.get()), "sunshine")); - - events_cb = std::make_unique>([this](ctx_t::pointer ctx) { - switch(pa_context_get_state(ctx)) { - case PA_CONTEXT_READY: - events->raise(ready); - break; - case PA_CONTEXT_TERMINATED: - BOOST_LOG(debug) << "Pulseadio context terminated"sv; - events->raise(terminated); - break; - case PA_CONTEXT_FAILED: - BOOST_LOG(debug) << "Pulseadio context failed"sv; - events->raise(failed); - break; - case PA_CONTEXT_CONNECTING: - BOOST_LOG(debug) << "Connecting to pulseaudio"sv; - case PA_CONTEXT_UNCONNECTED: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - break; - } + ss << "rate=48000 sink_name="sv << name << " format=s16le channels="sv << channels << " channel_map="sv; + std::for_each_n(mapping, channels - 1, [&ss](std::uint8_t pos) { + ss << pa_channel_position_to_string(position_mapping[pos]) << ','; }); - pa_context_set_state_callback(ctx.get(), ctx_state_cb, events_cb.get()); + ss << pa_channel_position_to_string(position_mapping[mapping[channels - 1]]); - auto status = pa_context_connect(ctx.get(), nullptr, PA_CONTEXT_NOFLAGS, nullptr); - if(status) { - BOOST_LOG(error) << "Couldn't connect to pulseaudio: "sv << pa_strerror(status); - return -1; - } + ss << " sink_properties=device.description="sv << name; + auto result = ss.str(); - worker = std::thread { - [](loop_t::pointer loop) { - int retval; - auto status = pa_mainloop_run(loop, &retval); - - if(status < 0) { - BOOST_LOG(error) << "Couldn't run pulseaudio main loop"sv; - return; - } - }, - loop.get() - }; - - auto event = events->pop(); - if(event == failed) { - return -1; - } - - return 0; + BOOST_LOG(debug) << "null-sink args: "sv << result; + return result; } - int load_null(const char *name, const std::uint8_t *channel_mapping, int channels) { - auto alarm = safe::make_alarm(); + struct mic_attr_t: public mic_t { + util::safe_ptr mic; - op_t op { - pa_context_load_module( - ctx.get(), - "module-null-sink", - to_string(name, channel_mapping, channels).c_str(), - cb_i, - alarm.get()), - }; + capture_e + sample(std::vector &sample_buf) override { + auto sample_size = sample_buf.size(); - alarm->wait(); - return *alarm->status(); - } + auto buf = sample_buf.data(); + int status; + if (pa_simple_read(mic.get(), buf, sample_size * 2, &status)) { + BOOST_LOG(error) << "pa_simple_read() failed: "sv << pa_strerror(status); - int unload_null(std::uint32_t i) { - if(i == PA_INVALID_INDEX) { - return 0; + return capture_e::error; + } + + return capture_e::ok; + } + }; + + std::unique_ptr + microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, std::string source_name) { + auto mic = std::make_unique(); + + pa_sample_spec ss { PA_SAMPLE_S16LE, sample_rate, (std::uint8_t) channels }; + pa_channel_map pa_map; + + pa_map.channels = channels; + std::for_each_n(pa_map.map, pa_map.channels, [mapping](auto &channel) mutable { + channel = position_mapping[*mapping++]; + }); + + pa_buffer_attr pa_attr = {}; + pa_attr.maxlength = frame_size * 8; + + int status; + + mic->mic.reset( + pa_simple_new(nullptr, "sunshine", + pa_stream_direction_t::PA_STREAM_RECORD, source_name.c_str(), + "sunshine-record", &ss, &pa_map, &pa_attr, &status)); + + if (!mic->mic) { + auto err_str = pa_strerror(status); + BOOST_LOG(error) << "pa_simple_new() failed: "sv << err_str; + return nullptr; } - auto alarm = safe::make_alarm(); - - op_t op { - pa_context_unload_module(ctx.get(), i, success_cb, alarm.get()) - }; - - alarm->wait(); - - if(*alarm->status()) { - BOOST_LOG(error) << "Couldn't unload null-sink with index ["sv << i << "]: "sv << pa_strerror(pa_context_errno(ctx.get())); - return -1; - } - - return 0; + return mic; } - std::optional sink_info() override { - constexpr auto stereo = "sink-sunshine-stereo"; - constexpr auto surround51 = "sink-sunshine-surround51"; - constexpr auto surround71 = "sink-sunshine-surround71"; + namespace pa { + template + struct add_const_helper; - auto alarm = safe::make_alarm(); + template + struct add_const_helper { + using type = const std::remove_pointer_t *; + }; - sink_t sink; + template + struct add_const_helper { + using type = const T *; + }; - // Count of all virtual sinks that are created by us - int nullcount = 0; + template + using add_const_t = typename add_const_helper, T>::type; - cb_t f = [&](ctx_t::pointer ctx, const pa_sink_info *sink_info, int eol) { - if(!sink_info) { - if(!eol) { - BOOST_LOG(error) << "Couldn't get pulseaudio sink info: "sv << pa_strerror(pa_context_errno(ctx)); + template + void + pa_free(T *p) { + pa_xfree(p); + } + using ctx_t = util::safe_ptr; + using loop_t = util::safe_ptr; + using op_t = util::safe_ptr; + using string_t = util::safe_ptr>; - alarm->ring(-1); - } + template + using cb_simple_t = std::function i)>; - alarm->ring(0); + template + void + cb(ctx_t::pointer ctx, add_const_t i, void *userdata) { + auto &f = *(cb_simple_t *) userdata; + + // Cannot similarly filter on eol here. Unless reported otherwise assume + // we have no need for special filtering like cb? + f(ctx, i); + } + + template + using cb_t = std::function i, int eol)>; + + template + void + cb(ctx_t::pointer ctx, add_const_t i, int eol, void *userdata) { + auto &f = *(cb_t *) userdata; + + // For some reason, pulseaudio calls this callback after disconnecting + if (i && eol) { return; } - // Ensure Sunshine won't create a sink that already exists. - if(!std::strcmp(sink_info->name, stereo)) { - index.stereo = sink_info->owner_module; - - ++nullcount; - } - else if(!std::strcmp(sink_info->name, surround51)) { - index.surround51 = sink_info->owner_module; - - ++nullcount; - } - else if(!std::strcmp(sink_info->name, surround71)) { - index.surround71 = sink_info->owner_module; - - ++nullcount; - } - }; - - op_t op { pa_context_get_sink_info_list(ctx.get(), cb, &f) }; - - if(!op) { - BOOST_LOG(error) << "Couldn't create card info operation: "sv << pa_strerror(pa_context_errno(ctx.get())); - - return std::nullopt; + f(ctx, i, eol); } - alarm->wait(); + void + cb_i(ctx_t::pointer ctx, std::uint32_t i, void *userdata) { + auto alarm = (safe::alarm_raw_t *) userdata; - if(*alarm->status()) { - return std::nullopt; + alarm->ring(i); } - auto sink_name = get_default_sink_name(); - sink.host = sink_name; + void + ctx_state_cb(ctx_t::pointer ctx, void *userdata) { + auto &f = *(std::function *) userdata; - if(index.stereo == PA_INVALID_INDEX) { - index.stereo = load_null(stereo, speaker::map_stereo, sizeof(speaker::map_stereo)); - if(index.stereo == PA_INVALID_INDEX) { - BOOST_LOG(warning) << "Couldn't create virtual sink for stereo: "sv << pa_strerror(pa_context_errno(ctx.get())); - } - else { - ++nullcount; - } + f(ctx); } - if(index.surround51 == PA_INVALID_INDEX) { - index.surround51 = load_null(surround51, speaker::map_surround51, sizeof(speaker::map_surround51)); - if(index.surround51 == PA_INVALID_INDEX) { - BOOST_LOG(warning) << "Couldn't create virtual sink for surround-51: "sv << pa_strerror(pa_context_errno(ctx.get())); - } - else { - ++nullcount; - } + void + success_cb(ctx_t::pointer ctx, int status, void *userdata) { + assert(userdata != nullptr); + + auto alarm = (safe::alarm_raw_t *) userdata; + alarm->ring(status ? 0 : 1); } - if(index.surround71 == PA_INVALID_INDEX) { - index.surround71 = load_null(surround71, speaker::map_surround71, sizeof(speaker::map_surround71)); - if(index.surround71 == PA_INVALID_INDEX) { - BOOST_LOG(warning) << "Couldn't create virtual sink for surround-71: "sv << pa_strerror(pa_context_errno(ctx.get())); - } - else { - ++nullcount; - } - } + class server_t: public audio_control_t { + enum ctx_event_e : int { + ready, + terminated, + failed + }; - if(sink_name.empty()) { - BOOST_LOG(warning) << "Couldn't find an active default sink. Continuing with virtual audio only."sv; - } + public: + loop_t loop; + ctx_t ctx; + std::string requested_sink; - if(nullcount == 3) { - sink.null = std::make_optional(sink_t::null_t { stereo, surround51, surround71 }); - } + struct { + std::uint32_t stereo = PA_INVALID_INDEX; + std::uint32_t surround51 = PA_INVALID_INDEX; + std::uint32_t surround71 = PA_INVALID_INDEX; + } index; - return std::make_optional(std::move(sink)); - } + std::unique_ptr> events; + std::unique_ptr> events_cb; - std::string get_default_sink_name() { - std::string sink_name; - auto alarm = safe::make_alarm(); + std::thread worker; + int + init() { + events = std::make_unique>(); + loop.reset(pa_mainloop_new()); + ctx.reset(pa_context_new(pa_mainloop_get_api(loop.get()), "sunshine")); - cb_simple_t server_f = [&](ctx_t::pointer ctx, const pa_server_info *server_info) { - if(!server_info) { - BOOST_LOG(error) << "Couldn't get pulseaudio server info: "sv << pa_strerror(pa_context_errno(ctx)); - alarm->ring(-1); - } + events_cb = std::make_unique>([this](ctx_t::pointer ctx) { + switch (pa_context_get_state(ctx)) { + case PA_CONTEXT_READY: + events->raise(ready); + break; + case PA_CONTEXT_TERMINATED: + BOOST_LOG(debug) << "Pulseadio context terminated"sv; + events->raise(terminated); + break; + case PA_CONTEXT_FAILED: + BOOST_LOG(debug) << "Pulseadio context failed"sv; + events->raise(failed); + break; + case PA_CONTEXT_CONNECTING: + BOOST_LOG(debug) << "Connecting to pulseaudio"sv; + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + } + }); - if(server_info->default_sink_name) { - sink_name = server_info->default_sink_name; - } - alarm->ring(0); - }; + pa_context_set_state_callback(ctx.get(), ctx_state_cb, events_cb.get()); - op_t server_op { pa_context_get_server_info(ctx.get(), cb, &server_f) }; - alarm->wait(); - // No need to check status. If it failed just return default name. - return sink_name; - } - - std::string get_monitor_name(const std::string &sink_name) { - std::string monitor_name; - auto alarm = safe::make_alarm(); - - if(sink_name.empty()) { - return monitor_name; - } - - cb_t sink_f = [&](ctx_t::pointer ctx, const pa_sink_info *sink_info, int eol) { - if(!sink_info) { - if(!eol) { - BOOST_LOG(error) << "Couldn't get pulseaudio sink info for ["sv << sink_name - << "]: "sv << pa_strerror(pa_context_errno(ctx)); - alarm->ring(-1); + auto status = pa_context_connect(ctx.get(), nullptr, PA_CONTEXT_NOFLAGS, nullptr); + if (status) { + BOOST_LOG(error) << "Couldn't connect to pulseaudio: "sv << pa_strerror(status); + return -1; } - alarm->ring(0); - return; + worker = std::thread { + [](loop_t::pointer loop) { + int retval; + auto status = pa_mainloop_run(loop, &retval); + + if (status < 0) { + BOOST_LOG(error) << "Couldn't run pulseaudio main loop"sv; + return; + } + }, + loop.get() + }; + + auto event = events->pop(); + if (event == failed) { + return -1; + } + + return 0; } - monitor_name = sink_info->monitor_source_name; + int + load_null(const char *name, const std::uint8_t *channel_mapping, int channels) { + auto alarm = safe::make_alarm(); + + op_t op { + pa_context_load_module( + ctx.get(), + "module-null-sink", + to_string(name, channel_mapping, channels).c_str(), + cb_i, + alarm.get()), + }; + + alarm->wait(); + return *alarm->status(); + } + + int + unload_null(std::uint32_t i) { + if (i == PA_INVALID_INDEX) { + return 0; + } + + auto alarm = safe::make_alarm(); + + op_t op { + pa_context_unload_module(ctx.get(), i, success_cb, alarm.get()) + }; + + alarm->wait(); + + if (*alarm->status()) { + BOOST_LOG(error) << "Couldn't unload null-sink with index ["sv << i << "]: "sv << pa_strerror(pa_context_errno(ctx.get())); + return -1; + } + + return 0; + } + + std::optional + sink_info() override { + constexpr auto stereo = "sink-sunshine-stereo"; + constexpr auto surround51 = "sink-sunshine-surround51"; + constexpr auto surround71 = "sink-sunshine-surround71"; + + auto alarm = safe::make_alarm(); + + sink_t sink; + + // Count of all virtual sinks that are created by us + int nullcount = 0; + + cb_t f = [&](ctx_t::pointer ctx, const pa_sink_info *sink_info, int eol) { + if (!sink_info) { + if (!eol) { + BOOST_LOG(error) << "Couldn't get pulseaudio sink info: "sv << pa_strerror(pa_context_errno(ctx)); + + alarm->ring(-1); + } + + alarm->ring(0); + return; + } + + // Ensure Sunshine won't create a sink that already exists. + if (!std::strcmp(sink_info->name, stereo)) { + index.stereo = sink_info->owner_module; + + ++nullcount; + } + else if (!std::strcmp(sink_info->name, surround51)) { + index.surround51 = sink_info->owner_module; + + ++nullcount; + } + else if (!std::strcmp(sink_info->name, surround71)) { + index.surround71 = sink_info->owner_module; + + ++nullcount; + } + }; + + op_t op { pa_context_get_sink_info_list(ctx.get(), cb, &f) }; + + if (!op) { + BOOST_LOG(error) << "Couldn't create card info operation: "sv << pa_strerror(pa_context_errno(ctx.get())); + + return std::nullopt; + } + + alarm->wait(); + + if (*alarm->status()) { + return std::nullopt; + } + + auto sink_name = get_default_sink_name(); + sink.host = sink_name; + + if (index.stereo == PA_INVALID_INDEX) { + index.stereo = load_null(stereo, speaker::map_stereo, sizeof(speaker::map_stereo)); + if (index.stereo == PA_INVALID_INDEX) { + BOOST_LOG(warning) << "Couldn't create virtual sink for stereo: "sv << pa_strerror(pa_context_errno(ctx.get())); + } + else { + ++nullcount; + } + } + + if (index.surround51 == PA_INVALID_INDEX) { + index.surround51 = load_null(surround51, speaker::map_surround51, sizeof(speaker::map_surround51)); + if (index.surround51 == PA_INVALID_INDEX) { + BOOST_LOG(warning) << "Couldn't create virtual sink for surround-51: "sv << pa_strerror(pa_context_errno(ctx.get())); + } + else { + ++nullcount; + } + } + + if (index.surround71 == PA_INVALID_INDEX) { + index.surround71 = load_null(surround71, speaker::map_surround71, sizeof(speaker::map_surround71)); + if (index.surround71 == PA_INVALID_INDEX) { + BOOST_LOG(warning) << "Couldn't create virtual sink for surround-71: "sv << pa_strerror(pa_context_errno(ctx.get())); + } + else { + ++nullcount; + } + } + + if (sink_name.empty()) { + BOOST_LOG(warning) << "Couldn't find an active default sink. Continuing with virtual audio only."sv; + } + + if (nullcount == 3) { + sink.null = std::make_optional(sink_t::null_t { stereo, surround51, surround71 }); + } + + return std::make_optional(std::move(sink)); + } + + std::string + get_default_sink_name() { + std::string sink_name; + auto alarm = safe::make_alarm(); + + cb_simple_t server_f = [&](ctx_t::pointer ctx, const pa_server_info *server_info) { + if (!server_info) { + BOOST_LOG(error) << "Couldn't get pulseaudio server info: "sv << pa_strerror(pa_context_errno(ctx)); + alarm->ring(-1); + } + + if (server_info->default_sink_name) { + sink_name = server_info->default_sink_name; + } + alarm->ring(0); + }; + + op_t server_op { pa_context_get_server_info(ctx.get(), cb, &server_f) }; + alarm->wait(); + // No need to check status. If it failed just return default name. + return sink_name; + } + + std::string + get_monitor_name(const std::string &sink_name) { + std::string monitor_name; + auto alarm = safe::make_alarm(); + + if (sink_name.empty()) { + return monitor_name; + } + + cb_t sink_f = [&](ctx_t::pointer ctx, const pa_sink_info *sink_info, int eol) { + if (!sink_info) { + if (!eol) { + BOOST_LOG(error) << "Couldn't get pulseaudio sink info for ["sv << sink_name + << "]: "sv << pa_strerror(pa_context_errno(ctx)); + alarm->ring(-1); + } + + alarm->ring(0); + return; + } + + monitor_name = sink_info->monitor_source_name; + }; + + op_t sink_op { pa_context_get_sink_info_by_name(ctx.get(), sink_name.c_str(), cb, &sink_f) }; + + alarm->wait(); + // No need to check status. If it failed just return default name. + BOOST_LOG(info) << "Found default monitor by name: "sv << monitor_name; + return monitor_name; + } + + std::unique_ptr + microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override { + // Sink choice priority: + // 1. Config sink + // 2. Last sink swapped to (Usually virtual in this case) + // 3. Default Sink + // An attempt was made to always use default to match the switching mechanic, + // but this happens right after the swap so the default returned by PA was not + // the new one just set! + auto sink_name = config::audio.sink; + if (sink_name.empty()) sink_name = requested_sink; + if (sink_name.empty()) sink_name = get_default_sink_name(); + + return ::platf::microphone(mapping, channels, sample_rate, frame_size, get_monitor_name(sink_name)); + } + + int + set_sink(const std::string &sink) override { + auto alarm = safe::make_alarm(); + + BOOST_LOG(info) << "Setting default sink to: ["sv << sink << "]"sv; + op_t op { + pa_context_set_default_sink( + ctx.get(), sink.c_str(), success_cb, alarm.get()), + }; + + if (!op) { + BOOST_LOG(error) << "Couldn't create set default-sink operation: "sv << pa_strerror(pa_context_errno(ctx.get())); + return -1; + } + + alarm->wait(); + if (*alarm->status()) { + BOOST_LOG(error) << "Couldn't set default-sink ["sv << sink << "]: "sv << pa_strerror(pa_context_errno(ctx.get())); + + return -1; + } + + requested_sink = sink; + + return 0; + } + + ~server_t() override { + unload_null(index.stereo); + unload_null(index.surround51); + unload_null(index.surround71); + + if (worker.joinable()) { + pa_context_disconnect(ctx.get()); + + KITTY_WHILE_LOOP(auto event = events->pop(), event != terminated && event != failed, { + event = events->pop(); + }) + + pa_mainloop_quit(loop.get(), 0); + worker.join(); + } + } }; + } // namespace pa - op_t sink_op { pa_context_get_sink_info_by_name(ctx.get(), sink_name.c_str(), cb, &sink_f) }; + std::unique_ptr + audio_control() { + auto audio = std::make_unique(); - alarm->wait(); - // No need to check status. If it failed just return default name. - BOOST_LOG(info) << "Found default monitor by name: "sv << monitor_name; - return monitor_name; - } - - std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override { - // Sink choice priority: - // 1. Config sink - // 2. Last sink swapped to (Usually virtual in this case) - // 3. Default Sink - // An attempt was made to always use default to match the switching mechanic, - // but this happens right after the swap so the default returned by PA was not - // the new one just set! - auto sink_name = config::audio.sink; - if(sink_name.empty()) sink_name = requested_sink; - if(sink_name.empty()) sink_name = get_default_sink_name(); - - return ::platf::microphone(mapping, channels, sample_rate, frame_size, get_monitor_name(sink_name)); - } - - int set_sink(const std::string &sink) override { - auto alarm = safe::make_alarm(); - - BOOST_LOG(info) << "Setting default sink to: ["sv << sink << "]"sv; - op_t op { - pa_context_set_default_sink( - ctx.get(), sink.c_str(), success_cb, alarm.get()), - }; - - if(!op) { - BOOST_LOG(error) << "Couldn't create set default-sink operation: "sv << pa_strerror(pa_context_errno(ctx.get())); - return -1; + if (audio->init()) { + return nullptr; } - alarm->wait(); - if(*alarm->status()) { - BOOST_LOG(error) << "Couldn't set default-sink ["sv << sink << "]: "sv << pa_strerror(pa_context_errno(ctx.get())); - - return -1; - } - - requested_sink = sink; - - return 0; + return audio; } - - ~server_t() override { - unload_null(index.stereo); - unload_null(index.surround51); - unload_null(index.surround71); - - if(worker.joinable()) { - pa_context_disconnect(ctx.get()); - - KITTY_WHILE_LOOP(auto event = events->pop(), event != terminated && event != failed, { - event = events->pop(); - }) - - pa_mainloop_quit(loop.get(), 0); - worker.join(); - } - } -}; -} // namespace pa - -std::unique_ptr audio_control() { - auto audio = std::make_unique(); - - if(audio->init()) { - return nullptr; - } - - return audio; -} -} // namespace platf \ No newline at end of file +} // namespace platf \ No newline at end of file diff --git a/src/platform/linux/cuda.cpp b/src/platform/linux/cuda.cpp index 845046d7..d1f8c148 100644 --- a/src/platform/linux/cuda.cpp +++ b/src/platform/linux/cuda.cpp @@ -20,700 +20,732 @@ extern "C" { #define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x) #define CU_CHECK(x, y) \ - if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return -1 + if (check((x), SUNSHINE_STRINGVIEW(y ": "))) return -1 #define CU_CHECK_IGNORE(x, y) \ check((x), SUNSHINE_STRINGVIEW(y ": ")) using namespace std::literals; namespace cuda { -constexpr auto cudaDevAttrMaxThreadsPerBlock = (CUdevice_attribute)1; -constexpr auto cudaDevAttrMaxThreadsPerMultiProcessor = (CUdevice_attribute)39; - -void pass_error(const std::string_view &sv, const char *name, const char *description) { - BOOST_LOG(error) << sv << name << ':' << description; -} - -void cff(CudaFunctions *cf) { - cuda_free_functions(&cf); -} - -using cdf_t = util::safe_ptr; - -static cdf_t cdf; - -inline static int check(CUresult result, const std::string_view &sv) { - if(result != CUDA_SUCCESS) { - const char *name; - const char *description; - - cdf->cuGetErrorName(result, &name); - cdf->cuGetErrorString(result, &description); + constexpr auto cudaDevAttrMaxThreadsPerBlock = (CUdevice_attribute) 1; + constexpr auto cudaDevAttrMaxThreadsPerMultiProcessor = (CUdevice_attribute) 39; + void + pass_error(const std::string_view &sv, const char *name, const char *description) { BOOST_LOG(error) << sv << name << ':' << description; - return -1; } - return 0; -} - -void freeStream(CUstream stream) { - CU_CHECK_IGNORE(cdf->cuStreamDestroy(stream), "Couldn't destroy cuda stream"); -} - -class img_t : public platf::img_t { -public: - tex_t tex; -}; - -int init() { - auto status = cuda_load_functions(&cdf, nullptr); - if(status) { - BOOST_LOG(error) << "Couldn't load cuda: "sv << status; - - return -1; + void + cff(CudaFunctions *cf) { + cuda_free_functions(&cf); } - CU_CHECK(cdf->cuInit(0), "Couldn't initialize cuda"); + using cdf_t = util::safe_ptr; - return 0; -} + static cdf_t cdf; -class cuda_t : public platf::hwdevice_t { -public: - int init(int in_width, int in_height) { - if(!cdf) { - BOOST_LOG(warning) << "cuda not initialized"sv; + inline static int + check(CUresult result, const std::string_view &sv) { + if (result != CUDA_SUCCESS) { + const char *name; + const char *description; + + cdf->cuGetErrorName(result, &name); + cdf->cuGetErrorString(result, &description); + + BOOST_LOG(error) << sv << name << ':' << description; return -1; } - data = (void *)0x1; - - width = in_width; - height = in_height; - return 0; } - int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { - this->hwframe.reset(frame); - this->frame = frame; + void + freeStream(CUstream stream) { + CU_CHECK_IGNORE(cdf->cuStreamDestroy(stream), "Couldn't destroy cuda stream"); + } + + class img_t: public platf::img_t { + public: + tex_t tex; + }; + + int + init() { + auto status = cuda_load_functions(&cdf, nullptr); + if (status) { + BOOST_LOG(error) << "Couldn't load cuda: "sv << status; - auto hwframe_ctx = (AVHWFramesContext *)hw_frames_ctx->data; - if(hwframe_ctx->sw_format != AV_PIX_FMT_NV12) { - BOOST_LOG(error) << "cuda::cuda_t doesn't support any format other than AV_PIX_FMT_NV12"sv; return -1; } - if(!frame->buf[0]) { - if(av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) { - BOOST_LOG(error) << "Couldn't get hwframe for NVENC"sv; + CU_CHECK(cdf->cuInit(0), "Couldn't initialize cuda"); + + return 0; + } + + class cuda_t: public platf::hwdevice_t { + public: + int + init(int in_width, int in_height) { + if (!cdf) { + BOOST_LOG(warning) << "cuda not initialized"sv; return -1; } - } - auto cuda_ctx = (AVCUDADeviceContext *)hwframe_ctx->device_ctx->hwctx; + data = (void *) 0x1; - stream = make_stream(); - if(!stream) { - return -1; - } + width = in_width; + height = in_height; - cuda_ctx->stream = stream.get(); - - auto sws_opt = sws_t::make(width, height, frame->width, frame->height, width * 4); - if(!sws_opt) { - return -1; - } - - sws = std::move(*sws_opt); - - linear_interpolation = width != frame->width || height != frame->height; - - return 0; - } - - void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override { - sws.set_colorspace(colorspace, color_range); - - auto tex = tex_t::make(height, width * 4); - if(!tex) { - return; - } - - // The default green color is ugly. - // Update the background color - platf::img_t img; - img.width = width; - img.height = height; - img.pixel_pitch = 4; - img.row_pitch = img.width * img.pixel_pitch; - - std::vector image_data; - image_data.resize(img.row_pitch * img.height); - - img.data = image_data.data(); - - if(sws.load_ram(img, tex->array)) { - return; - } - - sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex->texture.linear, stream.get(), { frame->width, frame->height, 0, 0 }); - } - - cudaTextureObject_t tex_obj(const tex_t &tex) const { - return linear_interpolation ? tex.texture.linear : tex.texture.point; - } - - stream_t stream; - frame_t hwframe; - - int width, height; - - // When heigth and width don't change, it's not necessary to use linear interpolation - bool linear_interpolation; - - sws_t sws; -}; - -class cuda_ram_t : public cuda_t { -public: - int convert(platf::img_t &img) override { - return sws.load_ram(img, tex.array) || sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(tex), stream.get()); - } - - int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { - if(cuda_t::set_frame(frame, hw_frames_ctx)) { - return -1; - } - - auto tex_opt = tex_t::make(height, width * 4); - if(!tex_opt) { - return -1; - } - - tex = std::move(*tex_opt); - - return 0; - } - - tex_t tex; -}; - -class cuda_vram_t : public cuda_t { -public: - int convert(platf::img_t &img) override { - return sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(((img_t *)&img)->tex), stream.get()); - } -}; - -std::shared_ptr make_hwdevice(int width, int height, bool vram) { - if(init()) { - return nullptr; - } - - std::shared_ptr cuda; - - if(vram) { - cuda = std::make_shared(); - } - else { - cuda = std::make_shared(); - } - - if(cuda->init(width, height)) { - return nullptr; - } - - return cuda; -} - -namespace nvfbc { -static PNVFBCCREATEINSTANCE createInstance {}; -static NVFBC_API_FUNCTION_LIST func { NVFBC_VERSION }; - -static constexpr inline NVFBC_BOOL nv_bool(bool b) { - return b ? NVFBC_TRUE : NVFBC_FALSE; -} - -static void *handle { nullptr }; -int init() { - static bool funcs_loaded = false; - - if(funcs_loaded) return 0; - - if(!handle) { - handle = dyn::handle({ "libnvidia-fbc.so.1", "libnvidia-fbc.so" }); - if(!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *)&createInstance, "NvFBCCreateInstance" }, - }; - - if(dyn::load(handle, funcs)) { - dlclose(handle); - handle = nullptr; - - return -1; - } - - auto status = cuda::nvfbc::createInstance(&cuda::nvfbc::func); - if(status) { - BOOST_LOG(error) << "Unable to create NvFBC instance"sv; - - dlclose(handle); - handle = nullptr; - return -1; - } - - funcs_loaded = true; - return 0; -} - -class ctx_t { -public: - ctx_t(NVFBC_SESSION_HANDLE handle) { - NVFBC_BIND_CONTEXT_PARAMS params { NVFBC_BIND_CONTEXT_PARAMS_VER }; - - if(func.nvFBCBindContext(handle, ¶ms)) { - BOOST_LOG(error) << "Couldn't bind NvFBC context to current thread: " << func.nvFBCGetLastErrorStr(handle); - } - - this->handle = handle; - } - - ~ctx_t() { - NVFBC_RELEASE_CONTEXT_PARAMS params { NVFBC_RELEASE_CONTEXT_PARAMS_VER }; - if(func.nvFBCReleaseContext(handle, ¶ms)) { - BOOST_LOG(error) << "Couldn't release NvFBC context from current thread: " << func.nvFBCGetLastErrorStr(handle); - } - } - - NVFBC_SESSION_HANDLE handle; -}; - -class handle_t { - enum flag_e { - SESSION_HANDLE, - SESSION_CAPTURE, - MAX_FLAGS, - }; - -public: - handle_t() = default; - handle_t(handle_t &&other) : handle_flags { other.handle_flags }, handle { other.handle } { - other.handle_flags.reset(); - } - - handle_t &operator=(handle_t &&other) { - std::swap(handle_flags, other.handle_flags); - std::swap(handle, other.handle); - - return *this; - } - - static std::optional make() { - NVFBC_CREATE_HANDLE_PARAMS params { NVFBC_CREATE_HANDLE_PARAMS_VER }; - - handle_t handle; - auto status = func.nvFBCCreateHandle(&handle.handle, ¶ms); - if(status) { - BOOST_LOG(error) << "Failed to create session: "sv << handle.last_error(); - - return std::nullopt; - } - - handle.handle_flags[SESSION_HANDLE] = true; - - return std::move(handle); - } - - const char *last_error() { - return func.nvFBCGetLastErrorStr(handle); - } - - std::optional status() { - NVFBC_GET_STATUS_PARAMS params { NVFBC_GET_STATUS_PARAMS_VER }; - - auto status = func.nvFBCGetStatus(handle, ¶ms); - if(status) { - BOOST_LOG(error) << "Failed to get NvFBC status: "sv << last_error(); - - return std::nullopt; - } - - return params; - } - - int capture(NVFBC_CREATE_CAPTURE_SESSION_PARAMS &capture_params) { - if(func.nvFBCCreateCaptureSession(handle, &capture_params)) { - BOOST_LOG(error) << "Failed to start capture session: "sv << last_error(); - return -1; - } - - handle_flags[SESSION_CAPTURE] = true; - - NVFBC_TOCUDA_SETUP_PARAMS setup_params { - NVFBC_TOCUDA_SETUP_PARAMS_VER, - NVFBC_BUFFER_FORMAT_BGRA, - }; - - if(func.nvFBCToCudaSetUp(handle, &setup_params)) { - BOOST_LOG(error) << "Failed to setup cuda interop with nvFBC: "sv << last_error(); - return -1; - } - return 0; - } - - int stop() { - if(!handle_flags[SESSION_CAPTURE]) { return 0; } - NVFBC_DESTROY_CAPTURE_SESSION_PARAMS params { NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER }; + int + set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { + this->hwframe.reset(frame); + this->frame = frame; - if(func.nvFBCDestroyCaptureSession(handle, ¶ms)) { - BOOST_LOG(error) << "Couldn't destroy capture session: "sv << last_error(); + auto hwframe_ctx = (AVHWFramesContext *) hw_frames_ctx->data; + if (hwframe_ctx->sw_format != AV_PIX_FMT_NV12) { + BOOST_LOG(error) << "cuda::cuda_t doesn't support any format other than AV_PIX_FMT_NV12"sv; + return -1; + } - return -1; - } + if (!frame->buf[0]) { + if (av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) { + BOOST_LOG(error) << "Couldn't get hwframe for NVENC"sv; + return -1; + } + } - handle_flags[SESSION_CAPTURE] = false; + auto cuda_ctx = (AVCUDADeviceContext *) hwframe_ctx->device_ctx->hwctx; - return 0; - } + stream = make_stream(); + if (!stream) { + return -1; + } + + cuda_ctx->stream = stream.get(); + + auto sws_opt = sws_t::make(width, height, frame->width, frame->height, width * 4); + if (!sws_opt) { + return -1; + } + + sws = std::move(*sws_opt); + + linear_interpolation = width != frame->width || height != frame->height; - int reset() { - if(!handle_flags[SESSION_HANDLE]) { return 0; } - stop(); + void + set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override { + sws.set_colorspace(colorspace, color_range); - NVFBC_DESTROY_HANDLE_PARAMS params { NVFBC_DESTROY_HANDLE_PARAMS_VER }; - - if(func.nvFBCDestroyHandle(handle, ¶ms)) { - BOOST_LOG(error) << "Couldn't destroy session handle: "sv << func.nvFBCGetLastErrorStr(handle); - } - - handle_flags[SESSION_HANDLE] = false; - - return 0; - } - - ~handle_t() { - reset(); - } - - std::bitset handle_flags; - - NVFBC_SESSION_HANDLE handle; -}; - -class display_t : public platf::display_t { -public: - int init(const std::string_view &display_name, const ::video::config_t &config) { - auto handle = handle_t::make(); - if(!handle) { - return -1; - } - - ctx_t ctx { handle->handle }; - - auto status_params = handle->status(); - if(!status_params) { - return -1; - } - - int streamedMonitor = -1; - if(!display_name.empty()) { - if(status_params->bXRandRAvailable) { - auto monitor_nr = util::from_view(display_name); - - if(monitor_nr < 0 || monitor_nr >= status_params->dwOutputNum) { - BOOST_LOG(warning) << "Can't stream monitor ["sv << monitor_nr << "], it needs to be between [0] and ["sv << status_params->dwOutputNum - 1 << "], defaulting to virtual desktop"sv; - } - else { - streamedMonitor = monitor_nr; - } + auto tex = tex_t::make(height, width * 4); + if (!tex) { + return; } - else { - BOOST_LOG(warning) << "XrandR not available, streaming entire virtual desktop"sv; + + // The default green color is ugly. + // Update the background color + platf::img_t img; + img.width = width; + img.height = height; + img.pixel_pitch = 4; + img.row_pitch = img.width * img.pixel_pitch; + + std::vector image_data; + image_data.resize(img.row_pitch * img.height); + + img.data = image_data.data(); + + if (sws.load_ram(img, tex->array)) { + return; } + + sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex->texture.linear, stream.get(), { frame->width, frame->height, 0, 0 }); } - delay = std::chrono::nanoseconds { 1s } / config.framerate; + cudaTextureObject_t + tex_obj(const tex_t &tex) const { + return linear_interpolation ? tex.texture.linear : tex.texture.point; + } - capture_params = NVFBC_CREATE_CAPTURE_SESSION_PARAMS { NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER }; + stream_t stream; + frame_t hwframe; - capture_params.eCaptureType = NVFBC_CAPTURE_SHARED_CUDA; - capture_params.bDisableAutoModesetRecovery = nv_bool(true); + int width, height; - capture_params.dwSamplingRateMs = 1000 /* ms */ / config.framerate; + // When heigth and width don't change, it's not necessary to use linear interpolation + bool linear_interpolation; - if(streamedMonitor != -1) { - auto &output = status_params->outputs[streamedMonitor]; + sws_t sws; + }; - width = output.trackedBox.w; - height = output.trackedBox.h; - offset_x = output.trackedBox.x; - offset_y = output.trackedBox.y; + class cuda_ram_t: public cuda_t { + public: + int + convert(platf::img_t &img) override { + return sws.load_ram(img, tex.array) || sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(tex), stream.get()); + } - capture_params.eTrackingType = NVFBC_TRACKING_OUTPUT; - capture_params.dwOutputId = output.dwId; + int + set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { + if (cuda_t::set_frame(frame, hw_frames_ctx)) { + return -1; + } + + auto tex_opt = tex_t::make(height, width * 4); + if (!tex_opt) { + return -1; + } + + tex = std::move(*tex_opt); + + return 0; + } + + tex_t tex; + }; + + class cuda_vram_t: public cuda_t { + public: + int + convert(platf::img_t &img) override { + return sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(((img_t *) &img)->tex), stream.get()); + } + }; + + std::shared_ptr + make_hwdevice(int width, int height, bool vram) { + if (init()) { + return nullptr; + } + + std::shared_ptr cuda; + + if (vram) { + cuda = std::make_shared(); } else { - capture_params.eTrackingType = NVFBC_TRACKING_SCREEN; - - width = status_params->screenSize.w; - height = status_params->screenSize.h; + cuda = std::make_shared(); } - env_width = status_params->screenSize.w; - env_height = status_params->screenSize.h; + if (cuda->init(width, height)) { + return nullptr; + } - this->handle = std::move(*handle); - return 0; + return cuda; } - platf::capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { - auto next_frame = std::chrono::steady_clock::now(); + namespace nvfbc { + static PNVFBCCREATEINSTANCE createInstance {}; + static NVFBC_API_FUNCTION_LIST func { NVFBC_VERSION }; - // Force display_t::capture to initialize handle_t::capture - cursor_visible = !*cursor; + static constexpr inline NVFBC_BOOL + nv_bool(bool b) { + return b ? NVFBC_TRUE : NVFBC_FALSE; + } - ctx_t ctx { handle.handle }; - auto fg = util::fail_guard([&]() { - handle.reset(); - }); + static void *handle { nullptr }; + int + init() { + static bool funcs_loaded = false; - while(img) { - auto now = std::chrono::steady_clock::now(); - if(next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + if (funcs_loaded) return 0; + + if (!handle) { + handle = dyn::handle({ "libnvidia-fbc.so.1", "libnvidia-fbc.so" }); + if (!handle) { + return -1; + } } - while(next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); - } - next_frame = now + delay; - auto status = snapshot(img.get(), 150ms, *cursor); - switch(status) { - case platf::capture_e::reinit: - case platf::capture_e::error: - return status; - case platf::capture_e::timeout: - img = snapshot_cb(img, false); - break; - case platf::capture_e::ok: - img = snapshot_cb(img, true); - break; - default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']'; - return status; - } - } - - return platf::capture_e::ok; - } - - // Reinitialize the capture session. - platf::capture_e reinit(bool cursor) { - if(handle.stop()) { - return platf::capture_e::error; - } - - cursor_visible = cursor; - if(cursor) { - capture_params.bPushModel = nv_bool(false); - capture_params.bWithCursor = nv_bool(true); - capture_params.bAllowDirectCapture = nv_bool(false); - } - else { - capture_params.bPushModel = nv_bool(true); - capture_params.bWithCursor = nv_bool(false); - capture_params.bAllowDirectCapture = nv_bool(true); - } - - if(handle.capture(capture_params)) { - return platf::capture_e::error; - } - - // If trying to capture directly, test if it actually does. - if(capture_params.bAllowDirectCapture) { - CUdeviceptr device_ptr; - NVFBC_FRAME_GRAB_INFO info; - - NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab { - NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER, - NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT, - &device_ptr, - &info, - 0, + std::vector> funcs { + { (dyn::apiproc *) &createInstance, "NvFBCCreateInstance" }, }; - // Direct Capture may fail the first few times, even if it's possible - for(int x = 0; x < 3; ++x) { - if(auto status = func.nvFBCToCudaGrabFrame(handle.handle, &grab)) { - if(status == NVFBC_ERR_MUST_RECREATE) { + if (dyn::load(handle, funcs)) { + dlclose(handle); + handle = nullptr; + + return -1; + } + + auto status = cuda::nvfbc::createInstance(&cuda::nvfbc::func); + if (status) { + BOOST_LOG(error) << "Unable to create NvFBC instance"sv; + + dlclose(handle); + handle = nullptr; + return -1; + } + + funcs_loaded = true; + return 0; + } + + class ctx_t { + public: + ctx_t(NVFBC_SESSION_HANDLE handle) { + NVFBC_BIND_CONTEXT_PARAMS params { NVFBC_BIND_CONTEXT_PARAMS_VER }; + + if (func.nvFBCBindContext(handle, ¶ms)) { + BOOST_LOG(error) << "Couldn't bind NvFBC context to current thread: " << func.nvFBCGetLastErrorStr(handle); + } + + this->handle = handle; + } + + ~ctx_t() { + NVFBC_RELEASE_CONTEXT_PARAMS params { NVFBC_RELEASE_CONTEXT_PARAMS_VER }; + if (func.nvFBCReleaseContext(handle, ¶ms)) { + BOOST_LOG(error) << "Couldn't release NvFBC context from current thread: " << func.nvFBCGetLastErrorStr(handle); + } + } + + NVFBC_SESSION_HANDLE handle; + }; + + class handle_t { + enum flag_e { + SESSION_HANDLE, + SESSION_CAPTURE, + MAX_FLAGS, + }; + + public: + handle_t() = default; + handle_t(handle_t &&other): + handle_flags { other.handle_flags }, handle { other.handle } { + other.handle_flags.reset(); + } + + handle_t & + operator=(handle_t &&other) { + std::swap(handle_flags, other.handle_flags); + std::swap(handle, other.handle); + + return *this; + } + + static std::optional + make() { + NVFBC_CREATE_HANDLE_PARAMS params { NVFBC_CREATE_HANDLE_PARAMS_VER }; + + handle_t handle; + auto status = func.nvFBCCreateHandle(&handle.handle, ¶ms); + if (status) { + BOOST_LOG(error) << "Failed to create session: "sv << handle.last_error(); + + return std::nullopt; + } + + handle.handle_flags[SESSION_HANDLE] = true; + + return std::move(handle); + } + + const char * + last_error() { + return func.nvFBCGetLastErrorStr(handle); + } + + std::optional + status() { + NVFBC_GET_STATUS_PARAMS params { NVFBC_GET_STATUS_PARAMS_VER }; + + auto status = func.nvFBCGetStatus(handle, ¶ms); + if (status) { + BOOST_LOG(error) << "Failed to get NvFBC status: "sv << last_error(); + + return std::nullopt; + } + + return params; + } + + int + capture(NVFBC_CREATE_CAPTURE_SESSION_PARAMS &capture_params) { + if (func.nvFBCCreateCaptureSession(handle, &capture_params)) { + BOOST_LOG(error) << "Failed to start capture session: "sv << last_error(); + return -1; + } + + handle_flags[SESSION_CAPTURE] = true; + + NVFBC_TOCUDA_SETUP_PARAMS setup_params { + NVFBC_TOCUDA_SETUP_PARAMS_VER, + NVFBC_BUFFER_FORMAT_BGRA, + }; + + if (func.nvFBCToCudaSetUp(handle, &setup_params)) { + BOOST_LOG(error) << "Failed to setup cuda interop with nvFBC: "sv << last_error(); + return -1; + } + return 0; + } + + int + stop() { + if (!handle_flags[SESSION_CAPTURE]) { + return 0; + } + + NVFBC_DESTROY_CAPTURE_SESSION_PARAMS params { NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER }; + + if (func.nvFBCDestroyCaptureSession(handle, ¶ms)) { + BOOST_LOG(error) << "Couldn't destroy capture session: "sv << last_error(); + + return -1; + } + + handle_flags[SESSION_CAPTURE] = false; + + return 0; + } + + int + reset() { + if (!handle_flags[SESSION_HANDLE]) { + return 0; + } + + stop(); + + NVFBC_DESTROY_HANDLE_PARAMS params { NVFBC_DESTROY_HANDLE_PARAMS_VER }; + + if (func.nvFBCDestroyHandle(handle, ¶ms)) { + BOOST_LOG(error) << "Couldn't destroy session handle: "sv << func.nvFBCGetLastErrorStr(handle); + } + + handle_flags[SESSION_HANDLE] = false; + + return 0; + } + + ~handle_t() { + reset(); + } + + std::bitset handle_flags; + + NVFBC_SESSION_HANDLE handle; + }; + + class display_t: public platf::display_t { + public: + int + init(const std::string_view &display_name, const ::video::config_t &config) { + auto handle = handle_t::make(); + if (!handle) { + return -1; + } + + ctx_t ctx { handle->handle }; + + auto status_params = handle->status(); + if (!status_params) { + return -1; + } + + int streamedMonitor = -1; + if (!display_name.empty()) { + if (status_params->bXRandRAvailable) { + auto monitor_nr = util::from_view(display_name); + + if (monitor_nr < 0 || monitor_nr >= status_params->dwOutputNum) { + BOOST_LOG(warning) << "Can't stream monitor ["sv << monitor_nr << "], it needs to be between [0] and ["sv << status_params->dwOutputNum - 1 << "], defaulting to virtual desktop"sv; + } + else { + streamedMonitor = monitor_nr; + } + } + else { + BOOST_LOG(warning) << "XrandR not available, streaming entire virtual desktop"sv; + } + } + + delay = std::chrono::nanoseconds { 1s } / config.framerate; + + capture_params = NVFBC_CREATE_CAPTURE_SESSION_PARAMS { NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER }; + + capture_params.eCaptureType = NVFBC_CAPTURE_SHARED_CUDA; + capture_params.bDisableAutoModesetRecovery = nv_bool(true); + + capture_params.dwSamplingRateMs = 1000 /* ms */ / config.framerate; + + if (streamedMonitor != -1) { + auto &output = status_params->outputs[streamedMonitor]; + + width = output.trackedBox.w; + height = output.trackedBox.h; + offset_x = output.trackedBox.x; + offset_y = output.trackedBox.y; + + capture_params.eTrackingType = NVFBC_TRACKING_OUTPUT; + capture_params.dwOutputId = output.dwId; + } + else { + capture_params.eTrackingType = NVFBC_TRACKING_SCREEN; + + width = status_params->screenSize.w; + height = status_params->screenSize.h; + } + + env_width = status_params->screenSize.w; + env_height = status_params->screenSize.h; + + this->handle = std::move(*handle); + return 0; + } + + platf::capture_e + capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { + auto next_frame = std::chrono::steady_clock::now(); + + // Force display_t::capture to initialize handle_t::capture + cursor_visible = !*cursor; + + ctx_t ctx { handle.handle }; + auto fg = util::fail_guard([&]() { + handle.reset(); + }); + + while (img) { + auto now = std::chrono::steady_clock::now(); + if (next_frame > now) { + std::this_thread::sleep_for((next_frame - now) / 3 * 2); + } + while (next_frame > now) { + std::this_thread::sleep_for(1ns); + now = std::chrono::steady_clock::now(); + } + next_frame = now + delay; + + auto status = snapshot(img.get(), 150ms, *cursor); + switch (status) { + case platf::capture_e::reinit: + case platf::capture_e::error: + return status; + case platf::capture_e::timeout: + img = snapshot_cb(img, false); + break; + case platf::capture_e::ok: + img = snapshot_cb(img, true); + break; + default: + BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; + return status; + } + } + + return platf::capture_e::ok; + } + + // Reinitialize the capture session. + platf::capture_e + reinit(bool cursor) { + if (handle.stop()) { + return platf::capture_e::error; + } + + cursor_visible = cursor; + if (cursor) { + capture_params.bPushModel = nv_bool(false); + capture_params.bWithCursor = nv_bool(true); + capture_params.bAllowDirectCapture = nv_bool(false); + } + else { + capture_params.bPushModel = nv_bool(true); + capture_params.bWithCursor = nv_bool(false); + capture_params.bAllowDirectCapture = nv_bool(true); + } + + if (handle.capture(capture_params)) { + return platf::capture_e::error; + } + + // If trying to capture directly, test if it actually does. + if (capture_params.bAllowDirectCapture) { + CUdeviceptr device_ptr; + NVFBC_FRAME_GRAB_INFO info; + + NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab { + NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER, + NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT, + &device_ptr, + &info, + 0, + }; + + // Direct Capture may fail the first few times, even if it's possible + for (int x = 0; x < 3; ++x) { + if (auto status = func.nvFBCToCudaGrabFrame(handle.handle, &grab)) { + if (status == NVFBC_ERR_MUST_RECREATE) { + return platf::capture_e::reinit; + } + + BOOST_LOG(error) << "Couldn't capture nvFramebuffer: "sv << handle.last_error(); + + return platf::capture_e::error; + } + + if (info.bDirectCapture) { + break; + } + + BOOST_LOG(debug) << "Direct capture failed attempt ["sv << x << ']'; + } + + if (!info.bDirectCapture) { + BOOST_LOG(debug) << "Direct capture failed, trying the extra copy method"sv; + // Direct capture failed + capture_params.bPushModel = nv_bool(false); + capture_params.bWithCursor = nv_bool(false); + capture_params.bAllowDirectCapture = nv_bool(false); + + if (handle.stop() || handle.capture(capture_params)) { + return platf::capture_e::error; + } + } + } + + return platf::capture_e::ok; + } + + platf::capture_e + snapshot(platf::img_t *img, std::chrono::milliseconds timeout, bool cursor) { + if (cursor != cursor_visible) { + auto status = reinit(cursor); + if (status != platf::capture_e::ok) { + return status; + } + } + + CUdeviceptr device_ptr; + NVFBC_FRAME_GRAB_INFO info; + + NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab { + NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER, + NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT, + &device_ptr, + &info, + (std::uint32_t) timeout.count(), + }; + + if (auto status = func.nvFBCToCudaGrabFrame(handle.handle, &grab)) { + if (status == NVFBC_ERR_MUST_RECREATE) { return platf::capture_e::reinit; } BOOST_LOG(error) << "Couldn't capture nvFramebuffer: "sv << handle.last_error(); - return platf::capture_e::error; } - if(info.bDirectCapture) { - break; - } - - BOOST_LOG(debug) << "Direct capture failed attempt ["sv << x << ']'; - } - - if(!info.bDirectCapture) { - BOOST_LOG(debug) << "Direct capture failed, trying the extra copy method"sv; - // Direct capture failed - capture_params.bPushModel = nv_bool(false); - capture_params.bWithCursor = nv_bool(false); - capture_params.bAllowDirectCapture = nv_bool(false); - - if(handle.stop() || handle.capture(capture_params)) { + if (((img_t *) img)->tex.copy((std::uint8_t *) device_ptr, img->height, img->row_pitch)) { return platf::capture_e::error; } + + return platf::capture_e::ok; } - } - return platf::capture_e::ok; - } - - platf::capture_e snapshot(platf::img_t *img, std::chrono::milliseconds timeout, bool cursor) { - if(cursor != cursor_visible) { - auto status = reinit(cursor); - if(status != platf::capture_e::ok) { - return status; + std::shared_ptr + make_hwdevice(platf::pix_fmt_e pix_fmt) override { + return ::cuda::make_hwdevice(width, height, true); } - } - CUdeviceptr device_ptr; - NVFBC_FRAME_GRAB_INFO info; + std::shared_ptr + alloc_img() override { + auto img = std::make_shared(); - NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab { - NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER, - NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT, - &device_ptr, - &info, - (std::uint32_t)timeout.count(), + img->data = nullptr; + img->width = width; + img->height = height; + img->pixel_pitch = 4; + img->row_pitch = img->width * img->pixel_pitch; + + auto tex_opt = tex_t::make(height, width * img->pixel_pitch); + if (!tex_opt) { + return nullptr; + } + + img->tex = std::move(*tex_opt); + + return img; + }; + + int + dummy_img(platf::img_t *) override { + return 0; + } + + std::chrono::nanoseconds delay; + + bool cursor_visible; + handle_t handle; + + NVFBC_CREATE_CAPTURE_SESSION_PARAMS capture_params; }; + } // namespace nvfbc +} // namespace cuda - if(auto status = func.nvFBCToCudaGrabFrame(handle.handle, &grab)) { - if(status == NVFBC_ERR_MUST_RECREATE) { - return platf::capture_e::reinit; - } - - BOOST_LOG(error) << "Couldn't capture nvFramebuffer: "sv << handle.last_error(); - return platf::capture_e::error; - } - - if(((img_t *)img)->tex.copy((std::uint8_t *)device_ptr, img->height, img->row_pitch)) { - return platf::capture_e::error; - } - - return platf::capture_e::ok; - } - - std::shared_ptr make_hwdevice(platf::pix_fmt_e pix_fmt) override { - return ::cuda::make_hwdevice(width, height, true); - } - - std::shared_ptr alloc_img() override { - auto img = std::make_shared(); - - img->data = nullptr; - img->width = width; - img->height = height; - img->pixel_pitch = 4; - img->row_pitch = img->width * img->pixel_pitch; - - auto tex_opt = tex_t::make(height, width * img->pixel_pitch); - if(!tex_opt) { +namespace platf { + std::shared_ptr + nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { + if (hwdevice_type != mem_type_e::cuda) { + BOOST_LOG(error) << "Could not initialize nvfbc display with the given hw device type"sv; return nullptr; } - img->tex = std::move(*tex_opt); + auto display = std::make_shared(); - return img; - }; + if (display->init(display_name, config)) { + return nullptr; + } - int dummy_img(platf::img_t *) override { - return 0; + return display; } - std::chrono::nanoseconds delay; + std::vector + nvfbc_display_names() { + if (cuda::init() || cuda::nvfbc::init()) { + return {}; + } - bool cursor_visible; - handle_t handle; + std::vector display_names; - NVFBC_CREATE_CAPTURE_SESSION_PARAMS capture_params; -}; -} // namespace nvfbc -} // namespace cuda + auto handle = cuda::nvfbc::handle_t::make(); + if (!handle) { + return {}; + } -namespace platf { -std::shared_ptr nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { - if(hwdevice_type != mem_type_e::cuda) { - BOOST_LOG(error) << "Could not initialize nvfbc display with the given hw device type"sv; - return nullptr; + auto status_params = handle->status(); + if (!status_params) { + return {}; + } + + if (!status_params->bIsCapturePossible) { + BOOST_LOG(error) << "NVidia driver doesn't support NvFBC screencasting"sv; + } + + BOOST_LOG(info) << "Found ["sv << status_params->dwOutputNum << "] outputs"sv; + BOOST_LOG(info) << "Virtual Desktop: "sv << status_params->screenSize.w << 'x' << status_params->screenSize.h; + BOOST_LOG(info) << "XrandR: "sv << (status_params->bXRandRAvailable ? "available"sv : "unavailable"sv); + + for (auto x = 0; x < status_params->dwOutputNum; ++x) { + auto &output = status_params->outputs[x]; + BOOST_LOG(info) << "-- Output --"sv; + BOOST_LOG(debug) << " ID: "sv << output.dwId; + BOOST_LOG(debug) << " Name: "sv << output.name; + BOOST_LOG(info) << " Resolution: "sv << output.trackedBox.w << 'x' << output.trackedBox.h; + BOOST_LOG(info) << " Offset: "sv << output.trackedBox.x << 'x' << output.trackedBox.y; + display_names.emplace_back(std::to_string(x)); + } + + return display_names; } - - auto display = std::make_shared(); - - if(display->init(display_name, config)) { - return nullptr; - } - - return display; -} - -std::vector nvfbc_display_names() { - if(cuda::init() || cuda::nvfbc::init()) { - return {}; - } - - std::vector display_names; - - auto handle = cuda::nvfbc::handle_t::make(); - if(!handle) { - return {}; - } - - auto status_params = handle->status(); - if(!status_params) { - return {}; - } - - if(!status_params->bIsCapturePossible) { - BOOST_LOG(error) << "NVidia driver doesn't support NvFBC screencasting"sv; - } - - BOOST_LOG(info) << "Found ["sv << status_params->dwOutputNum << "] outputs"sv; - BOOST_LOG(info) << "Virtual Desktop: "sv << status_params->screenSize.w << 'x' << status_params->screenSize.h; - BOOST_LOG(info) << "XrandR: "sv << (status_params->bXRandRAvailable ? "available"sv : "unavailable"sv); - - for(auto x = 0; x < status_params->dwOutputNum; ++x) { - auto &output = status_params->outputs[x]; - BOOST_LOG(info) << "-- Output --"sv; - BOOST_LOG(debug) << " ID: "sv << output.dwId; - BOOST_LOG(debug) << " Name: "sv << output.name; - BOOST_LOG(info) << " Resolution: "sv << output.trackedBox.w << 'x' << output.trackedBox.h; - BOOST_LOG(info) << " Offset: "sv << output.trackedBox.x << 'x' << output.trackedBox.y; - display_names.emplace_back(std::to_string(x)); - } - - return display_names; -} -} // namespace platf \ No newline at end of file +} // namespace platf \ No newline at end of file diff --git a/src/platform/linux/cuda.h b/src/platform/linux/cuda.h index b175140d..e3834940 100644 --- a/src/platform/linux/cuda.h +++ b/src/platform/linux/cuda.h @@ -1,107 +1,121 @@ #if !defined(SUNSHINE_PLATFORM_CUDA_H) && defined(SUNSHINE_BUILD_CUDA) -#define SUNSHINE_PLATFORM_CUDA_H + #define SUNSHINE_PLATFORM_CUDA_H -#include -#include -#include -#include + #include + #include + #include + #include namespace platf { -class hwdevice_t; -class img_t; -} // namespace platf + class hwdevice_t; + class img_t; +} // namespace platf namespace cuda { -namespace nvfbc { -std::vector display_names(); -} -std::shared_ptr make_hwdevice(int width, int height, bool vram); -int init(); -} // namespace cuda + namespace nvfbc { + std::vector + display_names(); + } + std::shared_ptr + make_hwdevice(int width, int height, bool vram); + int + init(); +} // namespace cuda typedef struct cudaArray *cudaArray_t; -#if !defined(__CUDACC__) + #if !defined(__CUDACC__) typedef struct CUstream_st *cudaStream_t; typedef unsigned long long cudaTextureObject_t; -#else /* defined(__CUDACC__) */ + #else /* defined(__CUDACC__) */ typedef __location__(device_builtin) struct CUstream_st *cudaStream_t; typedef __location__(device_builtin) unsigned long long cudaTextureObject_t; -#endif /* !defined(__CUDACC__) */ + #endif /* !defined(__CUDACC__) */ namespace cuda { -class freeCudaPtr_t { -public: - void operator()(void *ptr); -}; + class freeCudaPtr_t { + public: + void + operator()(void *ptr); + }; -class freeCudaStream_t { -public: - void operator()(cudaStream_t ptr); -}; + class freeCudaStream_t { + public: + void + operator()(cudaStream_t ptr); + }; -using ptr_t = std::unique_ptr; -using stream_t = std::unique_ptr; + using ptr_t = std::unique_ptr; + using stream_t = std::unique_ptr; -stream_t make_stream(int flags = 0); + stream_t + make_stream(int flags = 0); -struct viewport_t { - int width, height; - int offsetX, offsetY; -}; + struct viewport_t { + int width, height; + int offsetX, offsetY; + }; -class tex_t { -public: - static std::optional make(int height, int pitch); + class tex_t { + public: + static std::optional + make(int height, int pitch); - tex_t(); - tex_t(tex_t &&); + tex_t(); + tex_t(tex_t &&); - tex_t &operator=(tex_t &&other); + tex_t & + operator=(tex_t &&other); - ~tex_t(); + ~tex_t(); - int copy(std::uint8_t *src, int height, int pitch); + int + copy(std::uint8_t *src, int height, int pitch); - cudaArray_t array; + cudaArray_t array; - struct texture { - cudaTextureObject_t point; - cudaTextureObject_t linear; - } texture; -}; + struct texture { + cudaTextureObject_t point; + cudaTextureObject_t linear; + } texture; + }; -class sws_t { -public: - sws_t() = default; - sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix); + class sws_t { + public: + sws_t() = default; + sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix); - /** + /** * in_width, in_height -- The width and height of the captured image in pixels * out_width, out_height -- the width and height of the NV12 image in pixels * * pitch -- The size of a single row of pixels in bytes */ - static std::optional make(int in_width, int in_height, int out_width, int out_height, int pitch); + static std::optional + make(int in_width, int in_height, int out_width, int out_height, int pitch); - // Converts loaded image into a CUDevicePtr - int convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream); - int convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport); + // Converts loaded image into a CUDevicePtr + int + convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream); + int + convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport); - void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range); + void + set_colorspace(std::uint32_t colorspace, std::uint32_t color_range); - int load_ram(platf::img_t &img, cudaArray_t array); + int + load_ram(platf::img_t &img, cudaArray_t array); - ptr_t color_matrix; + ptr_t color_matrix; - int threadsPerBlock; + int threadsPerBlock; - viewport_t viewport; + viewport_t viewport; - float scale; -}; -} // namespace cuda + float scale; + }; +} // namespace cuda #endif \ No newline at end of file diff --git a/src/platform/linux/graphics.cpp b/src/platform/linux/graphics.cpp index 1dc25b07..51db0846 100644 --- a/src/platform/linux/graphics.cpp +++ b/src/platform/linux/graphics.cpp @@ -9,9 +9,9 @@ // They aren't likely to change any time soon. #define fourcc_code(a, b, c, d) ((std::uint32_t)(a) | ((std::uint32_t)(b) << 8) | \ ((std::uint32_t)(c) << 16) | ((std::uint32_t)(d) << 24)) -#define fourcc_mod_code(vendor, val) ((((uint64_t)vendor) << 56) | ((val)&0x00ffffffffffffffULL)) -#define DRM_FORMAT_R8 fourcc_code('R', '8', ' ', ' ') /* [7:0] R */ -#define DRM_FORMAT_GR88 fourcc_code('G', 'R', '8', '8') /* [15:0] G:R 8:8 little endian */ +#define fourcc_mod_code(vendor, val) ((((uint64_t) vendor) << 56) | ((val) &0x00ffffffffffffffULL)) +#define DRM_FORMAT_R8 fourcc_code('R', '8', ' ', ' ') /* [7:0] R */ +#define DRM_FORMAT_GR88 fourcc_code('G', 'R', '8', '8') /* [15:0] G:R 8:8 little endian */ #define DRM_FORMAT_ARGB8888 fourcc_code('A', 'R', '2', '4') /* [31:0] A:R:G:B 8:8:8:8 little endian */ #define DRM_FORMAT_XRGB8888 fourcc_code('X', 'R', '2', '4') /* [31:0] x:R:G:B 8:8:8:8 little endian */ #define DRM_FORMAT_XBGR8888 fourcc_code('X', 'B', '2', '4') /* [31:0] x:B:G:R 8:8:8:8 little endian */ @@ -21,849 +21,881 @@ using namespace std::literals; namespace gl { -GladGLContext ctx; + GladGLContext ctx; -void drain_errors(const std::string_view &prefix) { - GLenum err; - while((err = ctx.GetError()) != GL_NO_ERROR) { - BOOST_LOG(error) << "GL: "sv << prefix << ": ["sv << util::hex(err).to_string_view() << ']'; - } -} - -tex_t::~tex_t() { - if(!size() == 0) { - ctx.DeleteTextures(size(), begin()); - } -} - -tex_t tex_t::make(std::size_t count) { - tex_t textures { count }; - - ctx.GenTextures(textures.size(), textures.begin()); - - float color[] = { 0.0f, 0.0f, 0.0f, 1.0f }; - - for(auto tex : textures) { - gl::ctx.BindTexture(GL_TEXTURE_2D, tex); - gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // x - gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // y - gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - gl::ctx.TexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, color); - } - - return textures; -} - -frame_buf_t::~frame_buf_t() { - if(begin()) { - ctx.DeleteFramebuffers(size(), begin()); - } -} - -frame_buf_t frame_buf_t::make(std::size_t count) { - frame_buf_t frame_buf { count }; - - ctx.GenFramebuffers(frame_buf.size(), frame_buf.begin()); - - return frame_buf; -} - -void frame_buf_t::copy(int id, int texture, int offset_x, int offset_y, int width, int height) { - gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[id]); - gl::ctx.ReadBuffer(GL_COLOR_ATTACHMENT0 + id); - gl::ctx.BindTexture(GL_TEXTURE_2D, texture); - gl::ctx.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, offset_x, offset_y, width, height); -} - -std::string shader_t::err_str() { - int length; - ctx.GetShaderiv(handle(), GL_INFO_LOG_LENGTH, &length); - - std::string string; - string.resize(length); - - ctx.GetShaderInfoLog(handle(), length, &length, string.data()); - - string.resize(length - 1); - - return string; -} - -util::Either shader_t::compile(const std::string_view &source, GLenum type) { - shader_t shader; - - auto data = source.data(); - GLint length = source.length(); - - shader._shader.el = ctx.CreateShader(type); - ctx.ShaderSource(shader.handle(), 1, &data, &length); - ctx.CompileShader(shader.handle()); - - int status = 0; - ctx.GetShaderiv(shader.handle(), GL_COMPILE_STATUS, &status); - - if(!status) { - return shader.err_str(); - } - - return shader; -} - -GLuint shader_t::handle() const { - return _shader.el; -} - -buffer_t buffer_t::make(util::buffer_t &&offsets, const char *block, const std::string_view &data) { - buffer_t buffer; - buffer._block = block; - buffer._size = data.size(); - buffer._offsets = std::move(offsets); - - ctx.GenBuffers(1, &buffer._buffer.el); - ctx.BindBuffer(GL_UNIFORM_BUFFER, buffer.handle()); - ctx.BufferData(GL_UNIFORM_BUFFER, data.size(), (const std::uint8_t *)data.data(), GL_DYNAMIC_DRAW); - - return buffer; -} - -GLuint buffer_t::handle() const { - return _buffer.el; -} - -const char *buffer_t::block() const { - return _block; -} - -void buffer_t::update(const std::string_view &view, std::size_t offset) { - ctx.BindBuffer(GL_UNIFORM_BUFFER, handle()); - ctx.BufferSubData(GL_UNIFORM_BUFFER, offset, view.size(), (const void *)view.data()); -} - -void buffer_t::update(std::string_view *members, std::size_t count, std::size_t offset) { - util::buffer_t buffer { _size }; - - for(int x = 0; x < count; ++x) { - auto val = members[x]; - - std::copy_n((const std::uint8_t *)val.data(), val.size(), &buffer[_offsets[x]]); - } - - update(util::view(buffer.begin(), buffer.end()), offset); -} - -std::string program_t::err_str() { - int length; - ctx.GetProgramiv(handle(), GL_INFO_LOG_LENGTH, &length); - - std::string string; - string.resize(length); - - ctx.GetShaderInfoLog(handle(), length, &length, string.data()); - - string.resize(length - 1); - - return string; -} - -util::Either program_t::link(const shader_t &vert, const shader_t &frag) { - program_t program; - - program._program.el = ctx.CreateProgram(); - - ctx.AttachShader(program.handle(), vert.handle()); - ctx.AttachShader(program.handle(), frag.handle()); - - // p_handle stores a copy of the program handle, since program will be moved before - // the fail guard funcion is called. - auto fg = util::fail_guard([p_handle = program.handle(), &vert, &frag]() { - ctx.DetachShader(p_handle, vert.handle()); - ctx.DetachShader(p_handle, frag.handle()); - }); - - ctx.LinkProgram(program.handle()); - - int status = 0; - ctx.GetProgramiv(program.handle(), GL_LINK_STATUS, &status); - - if(!status) { - return program.err_str(); - } - - return program; -} - -void program_t::bind(const buffer_t &buffer) { - ctx.UseProgram(handle()); - auto i = ctx.GetUniformBlockIndex(handle(), buffer.block()); - - ctx.BindBufferBase(GL_UNIFORM_BUFFER, i, buffer.handle()); -} - -std::optional program_t::uniform(const char *block, std::pair *members, std::size_t count) { - auto i = ctx.GetUniformBlockIndex(handle(), block); - if(i == GL_INVALID_INDEX) { - BOOST_LOG(error) << "Couldn't find index of ["sv << block << ']'; - return std::nullopt; - } - - int size; - ctx.GetActiveUniformBlockiv(handle(), i, GL_UNIFORM_BLOCK_DATA_SIZE, &size); - - bool error_flag = false; - - util::buffer_t offsets { count }; - auto indices = (std::uint32_t *)alloca(count * sizeof(std::uint32_t)); - auto names = (const char **)alloca(count * sizeof(const char *)); - auto names_p = names; - - std::for_each_n(members, count, [names_p](auto &member) mutable { - *names_p++ = std::get<0>(member); - }); - - std::fill_n(indices, count, GL_INVALID_INDEX); - ctx.GetUniformIndices(handle(), count, names, indices); - - for(int x = 0; x < count; ++x) { - if(indices[x] == GL_INVALID_INDEX) { - error_flag = true; - - BOOST_LOG(error) << "Couldn't find ["sv << block << '.' << members[x].first << ']'; + void + drain_errors(const std::string_view &prefix) { + GLenum err; + while ((err = ctx.GetError()) != GL_NO_ERROR) { + BOOST_LOG(error) << "GL: "sv << prefix << ": ["sv << util::hex(err).to_string_view() << ']'; } } - if(error_flag) { - return std::nullopt; - } - - ctx.GetActiveUniformsiv(handle(), count, indices, GL_UNIFORM_OFFSET, offsets.begin()); - util::buffer_t buffer { (std::size_t)size }; - - for(int x = 0; x < count; ++x) { - auto val = std::get<1>(members[x]); - - std::copy_n((const std::uint8_t *)val.data(), val.size(), &buffer[offsets[x]]); - } - - return buffer_t::make(std::move(offsets), block, std::string_view { (char *)buffer.begin(), buffer.size() }); -} - -GLuint program_t::handle() const { - return _program.el; -} - -} // namespace gl - -namespace gbm { -device_destroy_fn device_destroy; -create_device_fn create_device; - -int init() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if(funcs_loaded) return 0; - - if(!handle) { - handle = dyn::handle({ "libgbm.so.1", "libgbm.so" }); - if(!handle) { - return -1; + tex_t::~tex_t() { + if (!size() == 0) { + ctx.DeleteTextures(size(), begin()); } } - std::vector> funcs { - { (GLADapiproc *)&device_destroy, "gbm_device_destroy" }, - { (GLADapiproc *)&create_device, "gbm_create_device" }, - }; + tex_t + tex_t::make(std::size_t count) { + tex_t textures { count }; - if(dyn::load(handle, funcs)) { - return -1; + ctx.GenTextures(textures.size(), textures.begin()); + + float color[] = { 0.0f, 0.0f, 0.0f, 1.0f }; + + for (auto tex : textures) { + gl::ctx.BindTexture(GL_TEXTURE_2D, tex); + gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // x + gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // y + gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + gl::ctx.TexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, color); + } + + return textures; } - funcs_loaded = true; - return 0; -} -} // namespace gbm - -namespace egl { -constexpr auto EGL_LINUX_DMA_BUF_EXT = 0x3270; -constexpr auto EGL_LINUX_DRM_FOURCC_EXT = 0x3271; -constexpr auto EGL_DMA_BUF_PLANE0_FD_EXT = 0x3272; -constexpr auto EGL_DMA_BUF_PLANE0_OFFSET_EXT = 0x3273; -constexpr auto EGL_DMA_BUF_PLANE0_PITCH_EXT = 0x3274; -constexpr auto EGL_DMA_BUF_PLANE1_FD_EXT = 0x3275; -constexpr auto EGL_DMA_BUF_PLANE1_OFFSET_EXT = 0x3276; -constexpr auto EGL_DMA_BUF_PLANE1_PITCH_EXT = 0x3277; -constexpr auto EGL_DMA_BUF_PLANE2_FD_EXT = 0x3278; -constexpr auto EGL_DMA_BUF_PLANE2_OFFSET_EXT = 0x3279; -constexpr auto EGL_DMA_BUF_PLANE2_PITCH_EXT = 0x327A; -constexpr auto EGL_DMA_BUF_PLANE3_FD_EXT = 0x3440; -constexpr auto EGL_DMA_BUF_PLANE3_OFFSET_EXT = 0x3441; -constexpr auto EGL_DMA_BUF_PLANE3_PITCH_EXT = 0x3442; -constexpr auto EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT = 0x3443; -constexpr auto EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT = 0x3444; -constexpr auto EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT = 0x3445; -constexpr auto EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT = 0x3446; -constexpr auto EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT = 0x3447; -constexpr auto EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT = 0x3448; -constexpr auto EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT = 0x3449; -constexpr auto EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT = 0x344A; - -bool fail() { - return eglGetError() != EGL_SUCCESS; -} - -display_t make_display(std::variant native_display) { - constexpr auto EGL_PLATFORM_GBM_MESA = 0x31D7; - constexpr auto EGL_PLATFORM_WAYLAND_KHR = 0x31D8; - constexpr auto EGL_PLATFORM_X11_KHR = 0x31D5; - - int egl_platform; - void *native_display_p; - - switch(native_display.index()) { - case 0: - egl_platform = EGL_PLATFORM_GBM_MESA; - native_display_p = std::get<0>(native_display); - break; - case 1: - egl_platform = EGL_PLATFORM_WAYLAND_KHR; - native_display_p = std::get<1>(native_display); - break; - case 2: - egl_platform = EGL_PLATFORM_X11_KHR; - native_display_p = std::get<2>(native_display); - break; - default: - BOOST_LOG(error) << "egl::make_display(): Index ["sv << native_display.index() << "] not implemented"sv; - return nullptr; - } - - // native_display.left() equals native_display.right() - display_t display = eglGetPlatformDisplay(egl_platform, native_display_p, nullptr); - - if(fail()) { - BOOST_LOG(error) << "Couldn't open EGL display: ["sv << util::hex(eglGetError()).to_string_view() << ']'; - return nullptr; - } - - int major, minor; - if(!eglInitialize(display.get(), &major, &minor)) { - BOOST_LOG(error) << "Couldn't initialize EGL display: ["sv << util::hex(eglGetError()).to_string_view() << ']'; - return nullptr; - } - - const char *extension_st = eglQueryString(display.get(), EGL_EXTENSIONS); - const char *version = eglQueryString(display.get(), EGL_VERSION); - const char *vendor = eglQueryString(display.get(), EGL_VENDOR); - const char *apis = eglQueryString(display.get(), EGL_CLIENT_APIS); - - BOOST_LOG(debug) << "EGL: ["sv << vendor << "]: version ["sv << version << ']'; - BOOST_LOG(debug) << "API's supported: ["sv << apis << ']'; - - const char *extensions[] { - "EGL_KHR_create_context", - "EGL_KHR_surfaceless_context", - "EGL_EXT_image_dma_buf_import", - "EGL_EXT_image_dma_buf_import_modifiers", - }; - - for(auto ext : extensions) { - if(!std::strstr(extension_st, ext)) { - BOOST_LOG(error) << "Missing extension: ["sv << ext << ']'; - return nullptr; + frame_buf_t::~frame_buf_t() { + if (begin()) { + ctx.DeleteFramebuffers(size(), begin()); } } - return display; -} + frame_buf_t + frame_buf_t::make(std::size_t count) { + frame_buf_t frame_buf { count }; -std::optional make_ctx(display_t::pointer display) { - constexpr int conf_attr[] { - EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE - }; + ctx.GenFramebuffers(frame_buf.size(), frame_buf.begin()); - int count; - EGLConfig conf; - if(!eglChooseConfig(display, conf_attr, &conf, 1, &count)) { - BOOST_LOG(error) << "Couldn't set config attributes: ["sv << util::hex(eglGetError()).to_string_view() << ']'; - return std::nullopt; + return frame_buf; } - if(!eglBindAPI(EGL_OPENGL_API)) { - BOOST_LOG(error) << "Couldn't bind API: ["sv << util::hex(eglGetError()).to_string_view() << ']'; - return std::nullopt; + void + frame_buf_t::copy(int id, int texture, int offset_x, int offset_y, int width, int height) { + gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[id]); + gl::ctx.ReadBuffer(GL_COLOR_ATTACHMENT0 + id); + gl::ctx.BindTexture(GL_TEXTURE_2D, texture); + gl::ctx.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, offset_x, offset_y, width, height); } - constexpr int attr[] { - EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE - }; + std::string + shader_t::err_str() { + int length; + ctx.GetShaderiv(handle(), GL_INFO_LOG_LENGTH, &length); - ctx_t ctx { display, eglCreateContext(display, conf, EGL_NO_CONTEXT, attr) }; - if(fail()) { - BOOST_LOG(error) << "Couldn't create EGL context: ["sv << util::hex(eglGetError()).to_string_view() << ']'; - return std::nullopt; + std::string string; + string.resize(length); + + ctx.GetShaderInfoLog(handle(), length, &length, string.data()); + + string.resize(length - 1); + + return string; } - TUPLE_EL_REF(ctx_p, 1, ctx.el); - if(!eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx_p)) { - BOOST_LOG(error) << "Couldn't make current display"sv; - return std::nullopt; - } + util::Either + shader_t::compile(const std::string_view &source, GLenum type) { + shader_t shader; - if(!gladLoadGLContext(&gl::ctx, eglGetProcAddress)) { - BOOST_LOG(error) << "Couldn't load OpenGL library"sv; - return std::nullopt; - } + auto data = source.data(); + GLint length = source.length(); - BOOST_LOG(debug) << "GL: vendor: "sv << gl::ctx.GetString(GL_VENDOR); - BOOST_LOG(debug) << "GL: renderer: "sv << gl::ctx.GetString(GL_RENDERER); - BOOST_LOG(debug) << "GL: version: "sv << gl::ctx.GetString(GL_VERSION); - BOOST_LOG(debug) << "GL: shader: "sv << gl::ctx.GetString(GL_SHADING_LANGUAGE_VERSION); + shader._shader.el = ctx.CreateShader(type); + ctx.ShaderSource(shader.handle(), 1, &data, &length); + ctx.CompileShader(shader.handle()); - gl::ctx.PixelStorei(GL_UNPACK_ALIGNMENT, 1); + int status = 0; + ctx.GetShaderiv(shader.handle(), GL_COMPILE_STATUS, &status); - return ctx; -} - -struct plane_attr_t { - EGLAttrib fd; - EGLAttrib offset; - EGLAttrib pitch; - EGLAttrib lo; - EGLAttrib hi; -}; - -inline plane_attr_t get_plane(std::uint32_t plane_indice) { - switch(plane_indice) { - case 0: - return { - EGL_DMA_BUF_PLANE0_FD_EXT, - EGL_DMA_BUF_PLANE0_OFFSET_EXT, - EGL_DMA_BUF_PLANE0_PITCH_EXT, - EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, - EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, - }; - case 1: - return { - EGL_DMA_BUF_PLANE1_FD_EXT, - EGL_DMA_BUF_PLANE1_OFFSET_EXT, - EGL_DMA_BUF_PLANE1_PITCH_EXT, - EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, - EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, - }; - case 2: - return { - EGL_DMA_BUF_PLANE2_FD_EXT, - EGL_DMA_BUF_PLANE2_OFFSET_EXT, - EGL_DMA_BUF_PLANE2_PITCH_EXT, - EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT, - EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT, - }; - case 3: - return { - EGL_DMA_BUF_PLANE3_FD_EXT, - EGL_DMA_BUF_PLANE3_OFFSET_EXT, - EGL_DMA_BUF_PLANE3_PITCH_EXT, - EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT, - EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT, - }; - } - - // Avoid warning - return {}; -} - -std::optional import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) { - EGLAttrib attribs[47]; - int atti = 0; - attribs[atti++] = EGL_WIDTH; - attribs[atti++] = xrgb.width; - attribs[atti++] = EGL_HEIGHT; - attribs[atti++] = xrgb.height; - attribs[atti++] = EGL_LINUX_DRM_FOURCC_EXT; - attribs[atti++] = xrgb.fourcc; - - for(auto x = 0; x < 4; ++x) { - auto fd = xrgb.fds[x]; - - if(fd < 0) { - continue; + if (!status) { + return shader.err_str(); } - auto plane_attr = get_plane(x); + return shader; + } - attribs[atti++] = plane_attr.fd; - attribs[atti++] = fd; - attribs[atti++] = plane_attr.offset; - attribs[atti++] = xrgb.offsets[x]; - attribs[atti++] = plane_attr.pitch; - attribs[atti++] = xrgb.pitches[x]; + GLuint + shader_t::handle() const { + return _shader.el; + } - if(xrgb.modifier != DRM_FORMAT_MOD_INVALID) { - attribs[atti++] = plane_attr.lo; - attribs[atti++] = xrgb.modifier & 0xFFFFFFFF; - attribs[atti++] = plane_attr.hi; - attribs[atti++] = xrgb.modifier >> 32; + buffer_t + buffer_t::make(util::buffer_t &&offsets, const char *block, const std::string_view &data) { + buffer_t buffer; + buffer._block = block; + buffer._size = data.size(); + buffer._offsets = std::move(offsets); + + ctx.GenBuffers(1, &buffer._buffer.el); + ctx.BindBuffer(GL_UNIFORM_BUFFER, buffer.handle()); + ctx.BufferData(GL_UNIFORM_BUFFER, data.size(), (const std::uint8_t *) data.data(), GL_DYNAMIC_DRAW); + + return buffer; + } + + GLuint + buffer_t::handle() const { + return _buffer.el; + } + + const char * + buffer_t::block() const { + return _block; + } + + void + buffer_t::update(const std::string_view &view, std::size_t offset) { + ctx.BindBuffer(GL_UNIFORM_BUFFER, handle()); + ctx.BufferSubData(GL_UNIFORM_BUFFER, offset, view.size(), (const void *) view.data()); + } + + void + buffer_t::update(std::string_view *members, std::size_t count, std::size_t offset) { + util::buffer_t buffer { _size }; + + for (int x = 0; x < count; ++x) { + auto val = members[x]; + + std::copy_n((const std::uint8_t *) val.data(), val.size(), &buffer[_offsets[x]]); } - } - attribs[atti++] = EGL_NONE; - rgb_t rgb { - egl_display, - eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs), - gl::tex_t::make(1) - }; - - if(!rgb->xrgb8) { - BOOST_LOG(error) << "Couldn't import RGB Image: "sv << util::hex(eglGetError()).to_string_view(); - - return std::nullopt; + update(util::view(buffer.begin(), buffer.end()), offset); } - gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]); - gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, rgb->xrgb8); + std::string + program_t::err_str() { + int length; + ctx.GetProgramiv(handle(), GL_INFO_LOG_LENGTH, &length); - gl::ctx.BindTexture(GL_TEXTURE_2D, 0); + std::string string; + string.resize(length); - gl_drain_errors; + ctx.GetShaderInfoLog(handle(), length, &length, string.data()); - return rgb; -} + string.resize(length - 1); -std::optional import_target(display_t::pointer egl_display, std::array &&fds, const surface_descriptor_t &r8, const surface_descriptor_t &gr88) { - EGLAttrib img_attr_planes[2][13] { - { EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_R8, - EGL_WIDTH, r8.width, - EGL_HEIGHT, r8.height, - EGL_DMA_BUF_PLANE0_FD_EXT, r8.fds[0], - EGL_DMA_BUF_PLANE0_OFFSET_EXT, r8.offsets[0], - EGL_DMA_BUF_PLANE0_PITCH_EXT, r8.pitches[0], - EGL_NONE }, - - { EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_GR88, - EGL_WIDTH, gr88.width, - EGL_HEIGHT, gr88.height, - EGL_DMA_BUF_PLANE0_FD_EXT, r8.fds[0], - EGL_DMA_BUF_PLANE0_OFFSET_EXT, gr88.offsets[0], - EGL_DMA_BUF_PLANE0_PITCH_EXT, gr88.pitches[0], - EGL_NONE }, - }; - - nv12_t nv12 { - egl_display, - eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, img_attr_planes[0]), - eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, img_attr_planes[1]), - gl::tex_t::make(2), - gl::frame_buf_t::make(2), - std::move(fds) - }; - - if(!nv12->r8 || !nv12->bg88) { - BOOST_LOG(error) << "Couldn't create KHR Image"sv; - - return std::nullopt; + return string; } - gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[0]); - gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, nv12->r8); + util::Either + program_t::link(const shader_t &vert, const shader_t &frag) { + program_t program; - gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[1]); - gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, nv12->bg88); + program._program.el = ctx.CreateProgram(); - nv12->buf.bind(std::begin(nv12->tex), std::end(nv12->tex)); + ctx.AttachShader(program.handle(), vert.handle()); + ctx.AttachShader(program.handle(), frag.handle()); - gl_drain_errors; + // p_handle stores a copy of the program handle, since program will be moved before + // the fail guard funcion is called. + auto fg = util::fail_guard([p_handle = program.handle(), &vert, &frag]() { + ctx.DetachShader(p_handle, vert.handle()); + ctx.DetachShader(p_handle, frag.handle()); + }); - return nv12; -} + ctx.LinkProgram(program.handle()); -void sws_t::set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) { - video::color_t *color_p; - switch(colorspace) { - case 5: // SWS_CS_SMPTE170M - color_p = &video::colors[0]; - break; - case 1: // SWS_CS_ITU709 - color_p = &video::colors[2]; - break; - case 9: // SWS_CS_BT2020 - color_p = &video::colors[4]; - break; - default: - BOOST_LOG(warning) << "Colorspace: ["sv << colorspace << "] not yet supported: switching to default"sv; - color_p = &video::colors[0]; - }; + int status = 0; + ctx.GetProgramiv(program.handle(), GL_LINK_STATUS, &status); - if(color_range > 1) { - // Full range - ++color_p; + if (!status) { + return program.err_str(); + } + + return program; } - std::string_view members[] { - util::view(color_p->color_vec_y), - util::view(color_p->color_vec_u), - util::view(color_p->color_vec_v), - util::view(color_p->range_y), - util::view(color_p->range_uv), - }; + void + program_t::bind(const buffer_t &buffer) { + ctx.UseProgram(handle()); + auto i = ctx.GetUniformBlockIndex(handle(), buffer.block()); - color_matrix.update(members, sizeof(members) / sizeof(decltype(members[0]))); + ctx.BindBufferBase(GL_UNIFORM_BUFFER, i, buffer.handle()); + } - program[0].bind(color_matrix); - program[1].bind(color_matrix); -} + std::optional + program_t::uniform(const char *block, std::pair *members, std::size_t count) { + auto i = ctx.GetUniformBlockIndex(handle(), block); + if (i == GL_INVALID_INDEX) { + BOOST_LOG(error) << "Couldn't find index of ["sv << block << ']'; + return std::nullopt; + } -std::optional sws_t::make(int in_width, int in_height, int out_width, int out_heigth, gl::tex_t &&tex) { - sws_t sws; - - sws.serial = std::numeric_limits::max(); - - // Ensure aspect ratio is maintained - auto scalar = std::fminf(out_width / (float)in_width, out_heigth / (float)in_height); - auto out_width_f = in_width * scalar; - auto out_height_f = in_height * scalar; - - // result is always positive - auto offsetX_f = (out_width - out_width_f) / 2; - auto offsetY_f = (out_heigth - out_height_f) / 2; - - sws.out_width = out_width_f; - sws.out_height = out_height_f; - - sws.in_width = in_width; - sws.in_height = in_height; - - sws.offsetX = offsetX_f; - sws.offsetY = offsetY_f; - - auto width_i = 1.0f / sws.out_width; - - { - const char *sources[] { - SUNSHINE_SHADERS_DIR "/ConvertUV.frag", - SUNSHINE_SHADERS_DIR "/ConvertUV.vert", - SUNSHINE_SHADERS_DIR "/ConvertY.frag", - SUNSHINE_SHADERS_DIR "/Scene.vert", - SUNSHINE_SHADERS_DIR "/Scene.frag", - }; - - GLenum shader_type[2] { - GL_FRAGMENT_SHADER, - GL_VERTEX_SHADER, - }; - - constexpr auto count = sizeof(sources) / sizeof(const char *); - - util::Either compiled_sources[count]; + int size; + ctx.GetActiveUniformBlockiv(handle(), i, GL_UNIFORM_BLOCK_DATA_SIZE, &size); bool error_flag = false; - for(int x = 0; x < count; ++x) { - auto &compiled_source = compiled_sources[x]; - compiled_source = gl::shader_t::compile(read_file(sources[x]), shader_type[x % 2]); - gl_drain_errors; + util::buffer_t offsets { count }; + auto indices = (std::uint32_t *) alloca(count * sizeof(std::uint32_t)); + auto names = (const char **) alloca(count * sizeof(const char *)); + auto names_p = names; - if(compiled_source.has_right()) { - BOOST_LOG(error) << sources[x] << ": "sv << compiled_source.right(); + std::for_each_n(members, count, [names_p](auto &member) mutable { + *names_p++ = std::get<0>(member); + }); + + std::fill_n(indices, count, GL_INVALID_INDEX); + ctx.GetUniformIndices(handle(), count, names, indices); + + for (int x = 0; x < count; ++x) { + if (indices[x] == GL_INVALID_INDEX) { error_flag = true; + + BOOST_LOG(error) << "Couldn't find ["sv << block << '.' << members[x].first << ']'; } } - if(error_flag) { + if (error_flag) { return std::nullopt; } - auto program = gl::program_t::link(compiled_sources[3].left(), compiled_sources[4].left()); - if(program.has_right()) { - BOOST_LOG(error) << "GL linker: "sv << program.right(); - return std::nullopt; + ctx.GetActiveUniformsiv(handle(), count, indices, GL_UNIFORM_OFFSET, offsets.begin()); + util::buffer_t buffer { (std::size_t) size }; + + for (int x = 0; x < count; ++x) { + auto val = std::get<1>(members[x]); + + std::copy_n((const std::uint8_t *) val.data(), val.size(), &buffer[offsets[x]]); } - // Cursor - shader - sws.program[2] = std::move(program.left()); + return buffer_t::make(std::move(offsets), block, std::string_view { (char *) buffer.begin(), buffer.size() }); + } - program = gl::program_t::link(compiled_sources[1].left(), compiled_sources[0].left()); - if(program.has_right()) { - BOOST_LOG(error) << "GL linker: "sv << program.right(); - return std::nullopt; + GLuint + program_t::handle() const { + return _program.el; + } + +} // namespace gl + +namespace gbm { + device_destroy_fn device_destroy; + create_device_fn create_device; + + int + init() { + static void *handle { nullptr }; + static bool funcs_loaded = false; + + if (funcs_loaded) return 0; + + if (!handle) { + handle = dyn::handle({ "libgbm.so.1", "libgbm.so" }); + if (!handle) { + return -1; + } } - // UV - shader - sws.program[1] = std::move(program.left()); + std::vector> funcs { + { (GLADapiproc *) &device_destroy, "gbm_device_destroy" }, + { (GLADapiproc *) &create_device, "gbm_create_device" }, + }; - program = gl::program_t::link(compiled_sources[3].left(), compiled_sources[2].left()); - if(program.has_right()) { - BOOST_LOG(error) << "GL linker: "sv << program.right(); - return std::nullopt; - } - - // Y - shader - sws.program[0] = std::move(program.left()); - } - - auto loc_width_i = gl::ctx.GetUniformLocation(sws.program[1].handle(), "width_i"); - if(loc_width_i < 0) { - BOOST_LOG(error) << "Couldn't find uniform [width_i]"sv; - return std::nullopt; - } - - gl::ctx.UseProgram(sws.program[1].handle()); - gl::ctx.Uniform1fv(loc_width_i, 1, &width_i); - - auto color_p = &video::colors[0]; - std::pair members[] { - std::make_pair("color_vec_y", util::view(color_p->color_vec_y)), - std::make_pair("color_vec_u", util::view(color_p->color_vec_u)), - std::make_pair("color_vec_v", util::view(color_p->color_vec_v)), - std::make_pair("range_y", util::view(color_p->range_y)), - std::make_pair("range_uv", util::view(color_p->range_uv)), - }; - - auto color_matrix = sws.program[0].uniform("ColorMatrix", members, sizeof(members) / sizeof(decltype(members[0]))); - if(!color_matrix) { - return std::nullopt; - } - - sws.color_matrix = std::move(*color_matrix); - - sws.tex = std::move(tex); - - sws.cursor_framebuffer = gl::frame_buf_t::make(1); - sws.cursor_framebuffer.bind(&sws.tex[0], &sws.tex[1]); - - sws.program[0].bind(sws.color_matrix); - sws.program[1].bind(sws.color_matrix); - - gl::ctx.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - gl_drain_errors; - - return std::move(sws); -} - -int sws_t::blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height) { - auto f = [&]() { - std::swap(offsetX, this->offsetX); - std::swap(offsetY, this->offsetY); - std::swap(width, this->out_width); - std::swap(height, this->out_height); - }; - - f(); - auto fg = util::fail_guard(f); - - return convert(fb); -} - -std::optional sws_t::make(int in_width, int in_height, int out_width, int out_heigth) { - auto tex = gl::tex_t::make(2); - gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]); - gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, in_width, in_height); - - return make(in_width, in_height, out_width, out_heigth, std::move(tex)); -} - -void sws_t::load_ram(platf::img_t &img) { - loaded_texture = tex[0]; - - gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture); - gl::ctx.TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, img.width, img.height, GL_BGRA, GL_UNSIGNED_BYTE, img.data); -} - -void sws_t::load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture) { - // When only a sub-part of the image must be encoded... - const bool copy = offset_x || offset_y || img.sd.width != in_width || img.sd.height != in_height; - if(copy) { - auto framebuf = gl::frame_buf_t::make(1); - framebuf.bind(&texture, &texture + 1); - - loaded_texture = tex[0]; - framebuf.copy(0, loaded_texture, offset_x, offset_y, in_width, in_height); - } - else { - loaded_texture = texture; - } - - if(img.data) { - GLenum attachment = GL_COLOR_ATTACHMENT0; - - gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, cursor_framebuffer[0]); - gl::ctx.UseProgram(program[2].handle()); - - // When a copy has already been made... - if(!copy) { - gl::ctx.BindTexture(GL_TEXTURE_2D, texture); - gl::ctx.DrawBuffers(1, &attachment); - - gl::ctx.Viewport(0, 0, in_width, in_height); - gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3); - - loaded_texture = tex[0]; - } - - gl::ctx.BindTexture(GL_TEXTURE_2D, tex[1]); - if(serial != img.serial) { - serial = img.serial; - - gl::ctx.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, img.width, img.height, 0, GL_BGRA, GL_UNSIGNED_BYTE, img.data); - } - - gl::ctx.Enable(GL_BLEND); - - gl::ctx.DrawBuffers(1, &attachment); - -#ifndef NDEBUG - auto status = gl::ctx.CheckFramebufferStatus(GL_FRAMEBUFFER); - if(status != GL_FRAMEBUFFER_COMPLETE) { - BOOST_LOG(error) << "Pass Cursor: CheckFramebufferStatus() --> [0x"sv << util::hex(status).to_string_view() << ']'; - return; - } -#endif - - gl::ctx.Viewport(img.x, img.y, img.width, img.height); - gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3); - - gl::ctx.Disable(GL_BLEND); - - gl::ctx.BindTexture(GL_TEXTURE_2D, 0); - gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0); - } -} - -int sws_t::convert(gl::frame_buf_t &fb) { - gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture); - - GLenum attachments[] { - GL_COLOR_ATTACHMENT0, - GL_COLOR_ATTACHMENT1 - }; - - for(int x = 0; x < sizeof(attachments) / sizeof(decltype(attachments[0])); ++x) { - gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, fb[x]); - gl::ctx.DrawBuffers(1, &attachments[x]); - -#ifndef NDEBUG - auto status = gl::ctx.CheckFramebufferStatus(GL_FRAMEBUFFER); - if(status != GL_FRAMEBUFFER_COMPLETE) { - BOOST_LOG(error) << "Pass "sv << x << ": CheckFramebufferStatus() --> [0x"sv << util::hex(status).to_string_view() << ']'; + if (dyn::load(handle, funcs)) { return -1; } -#endif - gl::ctx.UseProgram(program[x].handle()); - gl::ctx.Viewport(offsetX / (x + 1), offsetY / (x + 1), out_width / (x + 1), out_height / (x + 1)); - gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3); + funcs_loaded = true; + return 0; + } +} // namespace gbm + +namespace egl { + constexpr auto EGL_LINUX_DMA_BUF_EXT = 0x3270; + constexpr auto EGL_LINUX_DRM_FOURCC_EXT = 0x3271; + constexpr auto EGL_DMA_BUF_PLANE0_FD_EXT = 0x3272; + constexpr auto EGL_DMA_BUF_PLANE0_OFFSET_EXT = 0x3273; + constexpr auto EGL_DMA_BUF_PLANE0_PITCH_EXT = 0x3274; + constexpr auto EGL_DMA_BUF_PLANE1_FD_EXT = 0x3275; + constexpr auto EGL_DMA_BUF_PLANE1_OFFSET_EXT = 0x3276; + constexpr auto EGL_DMA_BUF_PLANE1_PITCH_EXT = 0x3277; + constexpr auto EGL_DMA_BUF_PLANE2_FD_EXT = 0x3278; + constexpr auto EGL_DMA_BUF_PLANE2_OFFSET_EXT = 0x3279; + constexpr auto EGL_DMA_BUF_PLANE2_PITCH_EXT = 0x327A; + constexpr auto EGL_DMA_BUF_PLANE3_FD_EXT = 0x3440; + constexpr auto EGL_DMA_BUF_PLANE3_OFFSET_EXT = 0x3441; + constexpr auto EGL_DMA_BUF_PLANE3_PITCH_EXT = 0x3442; + constexpr auto EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT = 0x3443; + constexpr auto EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT = 0x3444; + constexpr auto EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT = 0x3445; + constexpr auto EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT = 0x3446; + constexpr auto EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT = 0x3447; + constexpr auto EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT = 0x3448; + constexpr auto EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT = 0x3449; + constexpr auto EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT = 0x344A; + + bool + fail() { + return eglGetError() != EGL_SUCCESS; } - gl::ctx.BindTexture(GL_TEXTURE_2D, 0); + display_t + make_display(std::variant native_display) { + constexpr auto EGL_PLATFORM_GBM_MESA = 0x31D7; + constexpr auto EGL_PLATFORM_WAYLAND_KHR = 0x31D8; + constexpr auto EGL_PLATFORM_X11_KHR = 0x31D5; - gl::ctx.Flush(); + int egl_platform; + void *native_display_p; - return 0; -} -} // namespace egl + switch (native_display.index()) { + case 0: + egl_platform = EGL_PLATFORM_GBM_MESA; + native_display_p = std::get<0>(native_display); + break; + case 1: + egl_platform = EGL_PLATFORM_WAYLAND_KHR; + native_display_p = std::get<1>(native_display); + break; + case 2: + egl_platform = EGL_PLATFORM_X11_KHR; + native_display_p = std::get<2>(native_display); + break; + default: + BOOST_LOG(error) << "egl::make_display(): Index ["sv << native_display.index() << "] not implemented"sv; + return nullptr; + } -void free_frame(AVFrame *frame) { + // native_display.left() equals native_display.right() + display_t display = eglGetPlatformDisplay(egl_platform, native_display_p, nullptr); + + if (fail()) { + BOOST_LOG(error) << "Couldn't open EGL display: ["sv << util::hex(eglGetError()).to_string_view() << ']'; + return nullptr; + } + + int major, minor; + if (!eglInitialize(display.get(), &major, &minor)) { + BOOST_LOG(error) << "Couldn't initialize EGL display: ["sv << util::hex(eglGetError()).to_string_view() << ']'; + return nullptr; + } + + const char *extension_st = eglQueryString(display.get(), EGL_EXTENSIONS); + const char *version = eglQueryString(display.get(), EGL_VERSION); + const char *vendor = eglQueryString(display.get(), EGL_VENDOR); + const char *apis = eglQueryString(display.get(), EGL_CLIENT_APIS); + + BOOST_LOG(debug) << "EGL: ["sv << vendor << "]: version ["sv << version << ']'; + BOOST_LOG(debug) << "API's supported: ["sv << apis << ']'; + + const char *extensions[] { + "EGL_KHR_create_context", + "EGL_KHR_surfaceless_context", + "EGL_EXT_image_dma_buf_import", + "EGL_EXT_image_dma_buf_import_modifiers", + }; + + for (auto ext : extensions) { + if (!std::strstr(extension_st, ext)) { + BOOST_LOG(error) << "Missing extension: ["sv << ext << ']'; + return nullptr; + } + } + + return display; + } + + std::optional + make_ctx(display_t::pointer display) { + constexpr int conf_attr[] { + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE + }; + + int count; + EGLConfig conf; + if (!eglChooseConfig(display, conf_attr, &conf, 1, &count)) { + BOOST_LOG(error) << "Couldn't set config attributes: ["sv << util::hex(eglGetError()).to_string_view() << ']'; + return std::nullopt; + } + + if (!eglBindAPI(EGL_OPENGL_API)) { + BOOST_LOG(error) << "Couldn't bind API: ["sv << util::hex(eglGetError()).to_string_view() << ']'; + return std::nullopt; + } + + constexpr int attr[] { + EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE + }; + + ctx_t ctx { display, eglCreateContext(display, conf, EGL_NO_CONTEXT, attr) }; + if (fail()) { + BOOST_LOG(error) << "Couldn't create EGL context: ["sv << util::hex(eglGetError()).to_string_view() << ']'; + return std::nullopt; + } + + TUPLE_EL_REF(ctx_p, 1, ctx.el); + if (!eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx_p)) { + BOOST_LOG(error) << "Couldn't make current display"sv; + return std::nullopt; + } + + if (!gladLoadGLContext(&gl::ctx, eglGetProcAddress)) { + BOOST_LOG(error) << "Couldn't load OpenGL library"sv; + return std::nullopt; + } + + BOOST_LOG(debug) << "GL: vendor: "sv << gl::ctx.GetString(GL_VENDOR); + BOOST_LOG(debug) << "GL: renderer: "sv << gl::ctx.GetString(GL_RENDERER); + BOOST_LOG(debug) << "GL: version: "sv << gl::ctx.GetString(GL_VERSION); + BOOST_LOG(debug) << "GL: shader: "sv << gl::ctx.GetString(GL_SHADING_LANGUAGE_VERSION); + + gl::ctx.PixelStorei(GL_UNPACK_ALIGNMENT, 1); + + return ctx; + } + + struct plane_attr_t { + EGLAttrib fd; + EGLAttrib offset; + EGLAttrib pitch; + EGLAttrib lo; + EGLAttrib hi; + }; + + inline plane_attr_t + get_plane(std::uint32_t plane_indice) { + switch (plane_indice) { + case 0: + return { + EGL_DMA_BUF_PLANE0_FD_EXT, + EGL_DMA_BUF_PLANE0_OFFSET_EXT, + EGL_DMA_BUF_PLANE0_PITCH_EXT, + EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, + EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, + }; + case 1: + return { + EGL_DMA_BUF_PLANE1_FD_EXT, + EGL_DMA_BUF_PLANE1_OFFSET_EXT, + EGL_DMA_BUF_PLANE1_PITCH_EXT, + EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, + EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, + }; + case 2: + return { + EGL_DMA_BUF_PLANE2_FD_EXT, + EGL_DMA_BUF_PLANE2_OFFSET_EXT, + EGL_DMA_BUF_PLANE2_PITCH_EXT, + EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT, + EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT, + }; + case 3: + return { + EGL_DMA_BUF_PLANE3_FD_EXT, + EGL_DMA_BUF_PLANE3_OFFSET_EXT, + EGL_DMA_BUF_PLANE3_PITCH_EXT, + EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT, + EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT, + }; + } + + // Avoid warning + return {}; + } + + std::optional + import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) { + EGLAttrib attribs[47]; + int atti = 0; + attribs[atti++] = EGL_WIDTH; + attribs[atti++] = xrgb.width; + attribs[atti++] = EGL_HEIGHT; + attribs[atti++] = xrgb.height; + attribs[atti++] = EGL_LINUX_DRM_FOURCC_EXT; + attribs[atti++] = xrgb.fourcc; + + for (auto x = 0; x < 4; ++x) { + auto fd = xrgb.fds[x]; + + if (fd < 0) { + continue; + } + + auto plane_attr = get_plane(x); + + attribs[atti++] = plane_attr.fd; + attribs[atti++] = fd; + attribs[atti++] = plane_attr.offset; + attribs[atti++] = xrgb.offsets[x]; + attribs[atti++] = plane_attr.pitch; + attribs[atti++] = xrgb.pitches[x]; + + if (xrgb.modifier != DRM_FORMAT_MOD_INVALID) { + attribs[atti++] = plane_attr.lo; + attribs[atti++] = xrgb.modifier & 0xFFFFFFFF; + attribs[atti++] = plane_attr.hi; + attribs[atti++] = xrgb.modifier >> 32; + } + } + attribs[atti++] = EGL_NONE; + + rgb_t rgb { + egl_display, + eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs), + gl::tex_t::make(1) + }; + + if (!rgb->xrgb8) { + BOOST_LOG(error) << "Couldn't import RGB Image: "sv << util::hex(eglGetError()).to_string_view(); + + return std::nullopt; + } + + gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]); + gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, rgb->xrgb8); + + gl::ctx.BindTexture(GL_TEXTURE_2D, 0); + + gl_drain_errors; + + return rgb; + } + + std::optional + import_target(display_t::pointer egl_display, std::array &&fds, const surface_descriptor_t &r8, const surface_descriptor_t &gr88) { + EGLAttrib img_attr_planes[2][13] { + { EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_R8, + EGL_WIDTH, r8.width, + EGL_HEIGHT, r8.height, + EGL_DMA_BUF_PLANE0_FD_EXT, r8.fds[0], + EGL_DMA_BUF_PLANE0_OFFSET_EXT, r8.offsets[0], + EGL_DMA_BUF_PLANE0_PITCH_EXT, r8.pitches[0], + EGL_NONE }, + + { EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_GR88, + EGL_WIDTH, gr88.width, + EGL_HEIGHT, gr88.height, + EGL_DMA_BUF_PLANE0_FD_EXT, r8.fds[0], + EGL_DMA_BUF_PLANE0_OFFSET_EXT, gr88.offsets[0], + EGL_DMA_BUF_PLANE0_PITCH_EXT, gr88.pitches[0], + EGL_NONE }, + }; + + nv12_t nv12 { + egl_display, + eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, img_attr_planes[0]), + eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, img_attr_planes[1]), + gl::tex_t::make(2), + gl::frame_buf_t::make(2), + std::move(fds) + }; + + if (!nv12->r8 || !nv12->bg88) { + BOOST_LOG(error) << "Couldn't create KHR Image"sv; + + return std::nullopt; + } + + gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[0]); + gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, nv12->r8); + + gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[1]); + gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, nv12->bg88); + + nv12->buf.bind(std::begin(nv12->tex), std::end(nv12->tex)); + + gl_drain_errors; + + return nv12; + } + + void + sws_t::set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) { + video::color_t *color_p; + switch (colorspace) { + case 5: // SWS_CS_SMPTE170M + color_p = &video::colors[0]; + break; + case 1: // SWS_CS_ITU709 + color_p = &video::colors[2]; + break; + case 9: // SWS_CS_BT2020 + color_p = &video::colors[4]; + break; + default: + BOOST_LOG(warning) << "Colorspace: ["sv << colorspace << "] not yet supported: switching to default"sv; + color_p = &video::colors[0]; + }; + + if (color_range > 1) { + // Full range + ++color_p; + } + + std::string_view members[] { + util::view(color_p->color_vec_y), + util::view(color_p->color_vec_u), + util::view(color_p->color_vec_v), + util::view(color_p->range_y), + util::view(color_p->range_uv), + }; + + color_matrix.update(members, sizeof(members) / sizeof(decltype(members[0]))); + + program[0].bind(color_matrix); + program[1].bind(color_matrix); + } + + std::optional + sws_t::make(int in_width, int in_height, int out_width, int out_heigth, gl::tex_t &&tex) { + sws_t sws; + + sws.serial = std::numeric_limits::max(); + + // Ensure aspect ratio is maintained + auto scalar = std::fminf(out_width / (float) in_width, out_heigth / (float) in_height); + auto out_width_f = in_width * scalar; + auto out_height_f = in_height * scalar; + + // result is always positive + auto offsetX_f = (out_width - out_width_f) / 2; + auto offsetY_f = (out_heigth - out_height_f) / 2; + + sws.out_width = out_width_f; + sws.out_height = out_height_f; + + sws.in_width = in_width; + sws.in_height = in_height; + + sws.offsetX = offsetX_f; + sws.offsetY = offsetY_f; + + auto width_i = 1.0f / sws.out_width; + + { + const char *sources[] { + SUNSHINE_SHADERS_DIR "/ConvertUV.frag", + SUNSHINE_SHADERS_DIR "/ConvertUV.vert", + SUNSHINE_SHADERS_DIR "/ConvertY.frag", + SUNSHINE_SHADERS_DIR "/Scene.vert", + SUNSHINE_SHADERS_DIR "/Scene.frag", + }; + + GLenum shader_type[2] { + GL_FRAGMENT_SHADER, + GL_VERTEX_SHADER, + }; + + constexpr auto count = sizeof(sources) / sizeof(const char *); + + util::Either compiled_sources[count]; + + bool error_flag = false; + for (int x = 0; x < count; ++x) { + auto &compiled_source = compiled_sources[x]; + + compiled_source = gl::shader_t::compile(read_file(sources[x]), shader_type[x % 2]); + gl_drain_errors; + + if (compiled_source.has_right()) { + BOOST_LOG(error) << sources[x] << ": "sv << compiled_source.right(); + error_flag = true; + } + } + + if (error_flag) { + return std::nullopt; + } + + auto program = gl::program_t::link(compiled_sources[3].left(), compiled_sources[4].left()); + if (program.has_right()) { + BOOST_LOG(error) << "GL linker: "sv << program.right(); + return std::nullopt; + } + + // Cursor - shader + sws.program[2] = std::move(program.left()); + + program = gl::program_t::link(compiled_sources[1].left(), compiled_sources[0].left()); + if (program.has_right()) { + BOOST_LOG(error) << "GL linker: "sv << program.right(); + return std::nullopt; + } + + // UV - shader + sws.program[1] = std::move(program.left()); + + program = gl::program_t::link(compiled_sources[3].left(), compiled_sources[2].left()); + if (program.has_right()) { + BOOST_LOG(error) << "GL linker: "sv << program.right(); + return std::nullopt; + } + + // Y - shader + sws.program[0] = std::move(program.left()); + } + + auto loc_width_i = gl::ctx.GetUniformLocation(sws.program[1].handle(), "width_i"); + if (loc_width_i < 0) { + BOOST_LOG(error) << "Couldn't find uniform [width_i]"sv; + return std::nullopt; + } + + gl::ctx.UseProgram(sws.program[1].handle()); + gl::ctx.Uniform1fv(loc_width_i, 1, &width_i); + + auto color_p = &video::colors[0]; + std::pair members[] { + std::make_pair("color_vec_y", util::view(color_p->color_vec_y)), + std::make_pair("color_vec_u", util::view(color_p->color_vec_u)), + std::make_pair("color_vec_v", util::view(color_p->color_vec_v)), + std::make_pair("range_y", util::view(color_p->range_y)), + std::make_pair("range_uv", util::view(color_p->range_uv)), + }; + + auto color_matrix = sws.program[0].uniform("ColorMatrix", members, sizeof(members) / sizeof(decltype(members[0]))); + if (!color_matrix) { + return std::nullopt; + } + + sws.color_matrix = std::move(*color_matrix); + + sws.tex = std::move(tex); + + sws.cursor_framebuffer = gl::frame_buf_t::make(1); + sws.cursor_framebuffer.bind(&sws.tex[0], &sws.tex[1]); + + sws.program[0].bind(sws.color_matrix); + sws.program[1].bind(sws.color_matrix); + + gl::ctx.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + gl_drain_errors; + + return std::move(sws); + } + + int + sws_t::blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height) { + auto f = [&]() { + std::swap(offsetX, this->offsetX); + std::swap(offsetY, this->offsetY); + std::swap(width, this->out_width); + std::swap(height, this->out_height); + }; + + f(); + auto fg = util::fail_guard(f); + + return convert(fb); + } + + std::optional + sws_t::make(int in_width, int in_height, int out_width, int out_heigth) { + auto tex = gl::tex_t::make(2); + gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]); + gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, in_width, in_height); + + return make(in_width, in_height, out_width, out_heigth, std::move(tex)); + } + + void + sws_t::load_ram(platf::img_t &img) { + loaded_texture = tex[0]; + + gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture); + gl::ctx.TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, img.width, img.height, GL_BGRA, GL_UNSIGNED_BYTE, img.data); + } + + void + sws_t::load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture) { + // When only a sub-part of the image must be encoded... + const bool copy = offset_x || offset_y || img.sd.width != in_width || img.sd.height != in_height; + if (copy) { + auto framebuf = gl::frame_buf_t::make(1); + framebuf.bind(&texture, &texture + 1); + + loaded_texture = tex[0]; + framebuf.copy(0, loaded_texture, offset_x, offset_y, in_width, in_height); + } + else { + loaded_texture = texture; + } + + if (img.data) { + GLenum attachment = GL_COLOR_ATTACHMENT0; + + gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, cursor_framebuffer[0]); + gl::ctx.UseProgram(program[2].handle()); + + // When a copy has already been made... + if (!copy) { + gl::ctx.BindTexture(GL_TEXTURE_2D, texture); + gl::ctx.DrawBuffers(1, &attachment); + + gl::ctx.Viewport(0, 0, in_width, in_height); + gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3); + + loaded_texture = tex[0]; + } + + gl::ctx.BindTexture(GL_TEXTURE_2D, tex[1]); + if (serial != img.serial) { + serial = img.serial; + + gl::ctx.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, img.width, img.height, 0, GL_BGRA, GL_UNSIGNED_BYTE, img.data); + } + + gl::ctx.Enable(GL_BLEND); + + gl::ctx.DrawBuffers(1, &attachment); + +#ifndef NDEBUG + auto status = gl::ctx.CheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { + BOOST_LOG(error) << "Pass Cursor: CheckFramebufferStatus() --> [0x"sv << util::hex(status).to_string_view() << ']'; + return; + } +#endif + + gl::ctx.Viewport(img.x, img.y, img.width, img.height); + gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3); + + gl::ctx.Disable(GL_BLEND); + + gl::ctx.BindTexture(GL_TEXTURE_2D, 0); + gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0); + } + } + + int + sws_t::convert(gl::frame_buf_t &fb) { + gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture); + + GLenum attachments[] { + GL_COLOR_ATTACHMENT0, + GL_COLOR_ATTACHMENT1 + }; + + for (int x = 0; x < sizeof(attachments) / sizeof(decltype(attachments[0])); ++x) { + gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, fb[x]); + gl::ctx.DrawBuffers(1, &attachments[x]); + +#ifndef NDEBUG + auto status = gl::ctx.CheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { + BOOST_LOG(error) << "Pass "sv << x << ": CheckFramebufferStatus() --> [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } +#endif + + gl::ctx.UseProgram(program[x].handle()); + gl::ctx.Viewport(offsetX / (x + 1), offsetY / (x + 1), out_width / (x + 1), out_height / (x + 1)); + gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3); + } + + gl::ctx.BindTexture(GL_TEXTURE_2D, 0); + + gl::ctx.Flush(); + + return 0; + } +} // namespace egl + +void +free_frame(AVFrame *frame) { av_frame_free(&frame); } diff --git a/src/platform/linux/graphics.h b/src/platform/linux/graphics.h index dd53a7c8..01fc5304 100644 --- a/src/platform/linux/graphics.h +++ b/src/platform/linux/graphics.h @@ -17,303 +17,340 @@ #define gl_drain_errors_helper(x) gl::drain_errors(x) #define gl_drain_errors gl_drain_errors_helper(__FILE__ ":" SUNSHINE_STRINGIFY(__LINE__)) -extern "C" int close(int __fd); +extern "C" int +close(int __fd); // X11 Display extern "C" struct _XDisplay; struct AVFrame; -void free_frame(AVFrame *frame); +void +free_frame(AVFrame *frame); using frame_t = util::safe_ptr; namespace gl { -extern GladGLContext ctx; -void drain_errors(const std::string_view &prefix); + extern GladGLContext ctx; + void + drain_errors(const std::string_view &prefix); -class tex_t : public util::buffer_t { - using util::buffer_t::buffer_t; + class tex_t: public util::buffer_t { + using util::buffer_t::buffer_t; -public: - tex_t(tex_t &&) = default; - tex_t &operator=(tex_t &&) = default; + public: + tex_t(tex_t &&) = default; + tex_t & + operator=(tex_t &&) = default; - ~tex_t(); + ~tex_t(); - static tex_t make(std::size_t count); -}; + static tex_t + make(std::size_t count); + }; -class frame_buf_t : public util::buffer_t { - using util::buffer_t::buffer_t; + class frame_buf_t: public util::buffer_t { + using util::buffer_t::buffer_t; -public: - frame_buf_t(frame_buf_t &&) = default; - frame_buf_t &operator=(frame_buf_t &&) = default; + public: + frame_buf_t(frame_buf_t &&) = default; + frame_buf_t & + operator=(frame_buf_t &&) = default; - ~frame_buf_t(); + ~frame_buf_t(); - static frame_buf_t make(std::size_t count); + static frame_buf_t + make(std::size_t count); - inline void bind(std::nullptr_t, std::nullptr_t) { - int x = 0; - for(auto fb : (*this)) { - ctx.BindFramebuffer(GL_FRAMEBUFFER, fb); - ctx.FramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, 0, 0); + inline void + bind(std::nullptr_t, std::nullptr_t) { + int x = 0; + for (auto fb : (*this)) { + ctx.BindFramebuffer(GL_FRAMEBUFFER, fb); + ctx.FramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, 0, 0); - ++x; - } - return; - } - - template - void bind(It it_begin, It it_end) { - using namespace std::literals; - if(std::distance(it_begin, it_end) > size()) { - BOOST_LOG(warning) << "To many elements to bind"sv; + ++x; + } return; } - int x = 0; - std::for_each(it_begin, it_end, [&](auto tex) { - ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[x]); - ctx.BindTexture(GL_TEXTURE_2D, tex); + template + void + bind(It it_begin, It it_end) { + using namespace std::literals; + if (std::distance(it_begin, it_end) > size()) { + BOOST_LOG(warning) << "To many elements to bind"sv; + return; + } - ctx.FramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, tex, 0); + int x = 0; + std::for_each(it_begin, it_end, [&](auto tex) { + ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[x]); + ctx.BindTexture(GL_TEXTURE_2D, tex); - ++x; - }); - } + ctx.FramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, tex, 0); - /** + ++x; + }); + } + + /** * Copies a part of the framebuffer to texture */ - void copy(int id, int texture, int offset_x, int offset_y, int width, int height); -}; + void + copy(int id, int texture, int offset_x, int offset_y, int width, int height); + }; -class shader_t { - KITTY_USING_MOVE_T(shader_internal_t, GLuint, std::numeric_limits::max(), { - if(el != std::numeric_limits::max()) { - ctx.DeleteShader(el); - } - }); + class shader_t { + KITTY_USING_MOVE_T(shader_internal_t, GLuint, std::numeric_limits::max(), { + if (el != std::numeric_limits::max()) { + ctx.DeleteShader(el); + } + }); -public: - std::string err_str(); + public: + std::string + err_str(); - static util::Either compile(const std::string_view &source, GLenum type); + static util::Either + compile(const std::string_view &source, GLenum type); - GLuint handle() const; + GLuint + handle() const; -private: - shader_internal_t _shader; -}; + private: + shader_internal_t _shader; + }; -class buffer_t { - KITTY_USING_MOVE_T(buffer_internal_t, GLuint, std::numeric_limits::max(), { - if(el != std::numeric_limits::max()) { - ctx.DeleteBuffers(1, &el); - } - }); + class buffer_t { + KITTY_USING_MOVE_T(buffer_internal_t, GLuint, std::numeric_limits::max(), { + if (el != std::numeric_limits::max()) { + ctx.DeleteBuffers(1, &el); + } + }); -public: - static buffer_t make(util::buffer_t &&offsets, const char *block, const std::string_view &data); + public: + static buffer_t + make(util::buffer_t &&offsets, const char *block, const std::string_view &data); - GLuint handle() const; + GLuint + handle() const; - const char *block() const; + const char * + block() const; - void update(const std::string_view &view, std::size_t offset = 0); - void update(std::string_view *members, std::size_t count, std::size_t offset = 0); + void + update(const std::string_view &view, std::size_t offset = 0); + void + update(std::string_view *members, std::size_t count, std::size_t offset = 0); -private: - const char *_block; + private: + const char *_block; - std::size_t _size; + std::size_t _size; - util::buffer_t _offsets; + util::buffer_t _offsets; - buffer_internal_t _buffer; -}; + buffer_internal_t _buffer; + }; -class program_t { - KITTY_USING_MOVE_T(program_internal_t, GLuint, std::numeric_limits::max(), { - if(el != std::numeric_limits::max()) { - ctx.DeleteProgram(el); - } - }); + class program_t { + KITTY_USING_MOVE_T(program_internal_t, GLuint, std::numeric_limits::max(), { + if (el != std::numeric_limits::max()) { + ctx.DeleteProgram(el); + } + }); -public: - std::string err_str(); + public: + std::string + err_str(); - static util::Either link(const shader_t &vert, const shader_t &frag); + static util::Either + link(const shader_t &vert, const shader_t &frag); - void bind(const buffer_t &buffer); + void + bind(const buffer_t &buffer); - std::optional uniform(const char *block, std::pair *members, std::size_t count); + std::optional + uniform(const char *block, std::pair *members, std::size_t count); - GLuint handle() const; + GLuint + handle() const; -private: - program_internal_t _program; -}; -} // namespace gl + private: + program_internal_t _program; + }; +} // namespace gl namespace gbm { -struct device; -typedef void (*device_destroy_fn)(device *gbm); -typedef device *(*create_device_fn)(int fd); + struct device; + typedef void (*device_destroy_fn)(device *gbm); + typedef device *(*create_device_fn)(int fd); -extern device_destroy_fn device_destroy; -extern create_device_fn create_device; + extern device_destroy_fn device_destroy; + extern create_device_fn create_device; -using gbm_t = util::dyn_safe_ptr; + using gbm_t = util::dyn_safe_ptr; -int init(); + int + init(); -} // namespace gbm +} // namespace gbm namespace egl { -using display_t = util::dyn_safe_ptr_v2; + using display_t = util::dyn_safe_ptr_v2; -struct rgb_img_t { - display_t::pointer display; - EGLImage xrgb8; + struct rgb_img_t { + display_t::pointer display; + EGLImage xrgb8; - gl::tex_t tex; -}; + gl::tex_t tex; + }; -struct nv12_img_t { - display_t::pointer display; - EGLImage r8; - EGLImage bg88; + struct nv12_img_t { + display_t::pointer display; + EGLImage r8; + EGLImage bg88; - gl::tex_t tex; - gl::frame_buf_t buf; + gl::tex_t tex; + gl::frame_buf_t buf; - // sizeof(va::DRMPRIMESurfaceDescriptor::objects) / sizeof(va::DRMPRIMESurfaceDescriptor::objects[0]); - static constexpr std::size_t num_fds = 4; + // sizeof(va::DRMPRIMESurfaceDescriptor::objects) / sizeof(va::DRMPRIMESurfaceDescriptor::objects[0]); + static constexpr std::size_t num_fds = 4; - std::array fds; -}; + std::array fds; + }; -KITTY_USING_MOVE_T(rgb_t, rgb_img_t, , { - if(el.xrgb8) { - eglDestroyImage(el.display, el.xrgb8); - } -}); + KITTY_USING_MOVE_T(rgb_t, rgb_img_t, , { + if (el.xrgb8) { + eglDestroyImage(el.display, el.xrgb8); + } + }); -KITTY_USING_MOVE_T(nv12_t, nv12_img_t, , { - if(el.r8) { - eglDestroyImage(el.display, el.r8); - } + KITTY_USING_MOVE_T(nv12_t, nv12_img_t, , { + if (el.r8) { + eglDestroyImage(el.display, el.r8); + } - if(el.bg88) { - eglDestroyImage(el.display, el.bg88); - } -}); + if (el.bg88) { + eglDestroyImage(el.display, el.bg88); + } + }); -KITTY_USING_MOVE_T(ctx_t, (std::tuple), , { - TUPLE_2D_REF(disp, ctx, el); - if(ctx) { - eglMakeCurrent(disp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); - eglDestroyContext(disp, ctx); - } -}); + KITTY_USING_MOVE_T(ctx_t, (std::tuple), , { + TUPLE_2D_REF(disp, ctx, el); + if (ctx) { + eglMakeCurrent(disp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + eglDestroyContext(disp, ctx); + } + }); -struct surface_descriptor_t { - int width; - int height; - int fds[4]; - std::uint32_t fourcc; - std::uint64_t modifier; - std::uint32_t pitches[4]; - std::uint32_t offsets[4]; -}; + struct surface_descriptor_t { + int width; + int height; + int fds[4]; + std::uint32_t fourcc; + std::uint64_t modifier; + std::uint32_t pitches[4]; + std::uint32_t offsets[4]; + }; -display_t make_display(std::variant native_display); -std::optional make_ctx(display_t::pointer display); + display_t + make_display(std::variant native_display); + std::optional + make_ctx(display_t::pointer display); -std::optional import_source( - display_t::pointer egl_display, - const surface_descriptor_t &xrgb); + std::optional + import_source( + display_t::pointer egl_display, + const surface_descriptor_t &xrgb); -std::optional import_target( - display_t::pointer egl_display, - std::array &&fds, - const surface_descriptor_t &r8, const surface_descriptor_t &gr88); + std::optional + import_target( + display_t::pointer egl_display, + std::array &&fds, + const surface_descriptor_t &r8, const surface_descriptor_t &gr88); -class cursor_t : public platf::img_t { -public: - int x, y; + class cursor_t: public platf::img_t { + public: + int x, y; - unsigned long serial; + unsigned long serial; - std::vector buffer; -}; + std::vector buffer; + }; -// Allow cursor and the underlying image to be kept together -class img_descriptor_t : public cursor_t { -public: - ~img_descriptor_t() { - reset(); - } + // Allow cursor and the underlying image to be kept together + class img_descriptor_t: public cursor_t { + public: + ~img_descriptor_t() { + reset(); + } - void reset() { - for(auto x = 0; x < 4; ++x) { - if(sd.fds[x] >= 0) { - close(sd.fds[x]); + void + reset() { + for (auto x = 0; x < 4; ++x) { + if (sd.fds[x] >= 0) { + close(sd.fds[x]); - sd.fds[x] = -1; + sd.fds[x] = -1; + } } } - } - surface_descriptor_t sd; + surface_descriptor_t sd; - // Increment sequence when new rgb_t needs to be created - std::uint64_t sequence; -}; + // Increment sequence when new rgb_t needs to be created + std::uint64_t sequence; + }; -class sws_t { -public: - static std::optional make(int in_width, int in_height, int out_width, int out_heigth, gl::tex_t &&tex); - static std::optional make(int in_width, int in_height, int out_width, int out_heigth); + class sws_t { + public: + static std::optional + make(int in_width, int in_height, int out_width, int out_heigth, gl::tex_t &&tex); + static std::optional + make(int in_width, int in_height, int out_width, int out_heigth); - // Convert the loaded image into the first two framebuffers - int convert(gl::frame_buf_t &fb); + // Convert the loaded image into the first two framebuffers + int + convert(gl::frame_buf_t &fb); - // Make an area of the image black - int blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height); + // Make an area of the image black + int + blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height); - void load_ram(platf::img_t &img); - void load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture); + void + load_ram(platf::img_t &img); + void + load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture); - void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range); + void + set_colorspace(std::uint32_t colorspace, std::uint32_t color_range); - // The first texture is the monitor image. - // The second texture is the cursor image - gl::tex_t tex; + // The first texture is the monitor image. + // The second texture is the cursor image + gl::tex_t tex; - // The cursor image will be blended into this framebuffer - gl::frame_buf_t cursor_framebuffer; - gl::frame_buf_t copy_framebuffer; + // The cursor image will be blended into this framebuffer + gl::frame_buf_t cursor_framebuffer; + gl::frame_buf_t copy_framebuffer; - // Y - shader, UV - shader, Cursor - shader - gl::program_t program[3]; - gl::buffer_t color_matrix; + // Y - shader, UV - shader, Cursor - shader + gl::program_t program[3]; + gl::buffer_t color_matrix; - int out_width, out_height; - int in_width, in_height; - int offsetX, offsetY; + int out_width, out_height; + int in_width, in_height; + int offsetX, offsetY; - // Pointer to the texture to be converted to nv12 - int loaded_texture; + // Pointer to the texture to be converted to nv12 + int loaded_texture; - // Store latest cursor for load_vram - std::uint64_t serial; -}; + // Store latest cursor for load_vram + std::uint64_t serial; + }; -bool fail(); -} // namespace egl + bool + fail(); +} // namespace egl #endif diff --git a/src/platform/linux/input.cpp b/src/platform/linux/input.cpp index b37238da..5749b307 100644 --- a/src/platform/linux/input.cpp +++ b/src/platform/linux/input.cpp @@ -6,10 +6,10 @@ #include #ifdef SUNSHINE_BUILD_X11 -#include -#include -#include -#include + #include + #include + #include + #include #endif #include @@ -27,132 +27,135 @@ // Support older versions #ifndef REL_HWHEEL_HI_RES -#define REL_HWHEEL_HI_RES 0x0c + #define REL_HWHEEL_HI_RES 0x0c #endif #ifndef REL_WHEEL_HI_RES -#define REL_WHEEL_HI_RES 0x0b + #define REL_WHEEL_HI_RES 0x0b #endif using namespace std::literals; namespace platf { #ifdef SUNSHINE_BUILD_X11 -namespace x11 { -#define _FN(x, ret, args) \ - typedef ret(*x##_fn) args; \ - static x##_fn x + namespace x11 { + #define _FN(x, ret, args) \ + typedef ret(*x##_fn) args; \ + static x##_fn x -_FN(OpenDisplay, Display *, (_Xconst char *display_name)); -_FN(CloseDisplay, int, (Display * display)); -_FN(InitThreads, Status, (void)); -_FN(Flush, int, (Display *)); + _FN(OpenDisplay, Display *, (_Xconst char *display_name)); + _FN(CloseDisplay, int, (Display * display)); + _FN(InitThreads, Status, (void) ); + _FN(Flush, int, (Display *) ); -namespace tst { -_FN(FakeMotionEvent, int, (Display * dpy, int screen_numer, int x, int y, unsigned long delay)); -_FN(FakeRelativeMotionEvent, int, (Display * dpy, int deltaX, int deltaY, unsigned long delay)); -_FN(FakeButtonEvent, int, (Display * dpy, unsigned int button, Bool is_press, unsigned long delay)); -_FN(FakeKeyEvent, int, (Display * dpy, unsigned int keycode, Bool is_press, unsigned long delay)); + namespace tst { + _FN(FakeMotionEvent, int, (Display * dpy, int screen_numer, int x, int y, unsigned long delay)); + _FN(FakeRelativeMotionEvent, int, (Display * dpy, int deltaX, int deltaY, unsigned long delay)); + _FN(FakeButtonEvent, int, (Display * dpy, unsigned int button, Bool is_press, unsigned long delay)); + _FN(FakeKeyEvent, int, (Display * dpy, unsigned int keycode, Bool is_press, unsigned long delay)); -static int init() { - static void *handle { nullptr }; - static bool funcs_loaded = false; + static int + init() { + static void *handle { nullptr }; + static bool funcs_loaded = false; - if(funcs_loaded) return 0; + if (funcs_loaded) return 0; - if(!handle) { - handle = dyn::handle({ "libXtst.so.6", "libXtst.so" }); - if(!handle) { - return -1; + if (!handle) { + handle = dyn::handle({ "libXtst.so.6", "libXtst.so" }); + if (!handle) { + return -1; + } + } + + std::vector> funcs { + { (dyn::apiproc *) &FakeMotionEvent, "XTestFakeMotionEvent" }, + { (dyn::apiproc *) &FakeRelativeMotionEvent, "XTestFakeRelativeMotionEvent" }, + { (dyn::apiproc *) &FakeButtonEvent, "XTestFakeButtonEvent" }, + { (dyn::apiproc *) &FakeKeyEvent, "XTestFakeKeyEvent" }, + }; + + if (dyn::load(handle, funcs)) { + return -1; + } + + funcs_loaded = true; + return 0; + } + } // namespace tst + + static int + init() { + static void *handle { nullptr }; + static bool funcs_loaded = false; + + if (funcs_loaded) return 0; + + if (!handle) { + handle = dyn::handle({ "libX11.so.6", "libX11.so" }); + if (!handle) { + return -1; + } + } + + std::vector> funcs { + { (dyn::apiproc *) &OpenDisplay, "XOpenDisplay" }, + { (dyn::apiproc *) &CloseDisplay, "XCloseDisplay" }, + { (dyn::apiproc *) &InitThreads, "XInitThreads" }, + { (dyn::apiproc *) &Flush, "XFlush" }, + }; + + if (dyn::load(handle, funcs)) { + return -1; + } + + funcs_loaded = true; + return 0; } - } - - std::vector> funcs { - { (dyn::apiproc *)&FakeMotionEvent, "XTestFakeMotionEvent" }, - { (dyn::apiproc *)&FakeRelativeMotionEvent, "XTestFakeRelativeMotionEvent" }, - { (dyn::apiproc *)&FakeButtonEvent, "XTestFakeButtonEvent" }, - { (dyn::apiproc *)&FakeKeyEvent, "XTestFakeKeyEvent" }, - }; - - if(dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; -} -} // namespace tst - -static int init() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if(funcs_loaded) return 0; - - if(!handle) { - handle = dyn::handle({ "libX11.so.6", "libX11.so" }); - if(!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *)&OpenDisplay, "XOpenDisplay" }, - { (dyn::apiproc *)&CloseDisplay, "XCloseDisplay" }, - { (dyn::apiproc *)&InitThreads, "XInitThreads" }, - { (dyn::apiproc *)&Flush, "XFlush" }, - }; - - if(dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; -} -} // namespace x11 + } // namespace x11 #endif -constexpr auto mail_evdev = "platf::evdev"sv; + constexpr auto mail_evdev = "platf::evdev"sv; -using evdev_t = util::safe_ptr; -using uinput_t = util::safe_ptr; + using evdev_t = util::safe_ptr; + using uinput_t = util::safe_ptr; -constexpr pollfd read_pollfd { -1, 0, 0 }; -KITTY_USING_MOVE_T(pollfd_t, pollfd, read_pollfd, { - if(el.fd >= 0) { - ioctl(el.fd, EVIOCGRAB, (void *)0); + constexpr pollfd read_pollfd { -1, 0, 0 }; + KITTY_USING_MOVE_T(pollfd_t, pollfd, read_pollfd, { + if (el.fd >= 0) { + ioctl(el.fd, EVIOCGRAB, (void *) 0); - close(el.fd); - } -}); + close(el.fd); + } + }); -using mail_evdev_t = std::tuple; + using mail_evdev_t = std::tuple; -struct keycode_t { - std::uint32_t keycode; - std::uint32_t scancode; + struct keycode_t { + std::uint32_t keycode; + std::uint32_t scancode; #ifdef SUNSHINE_BUILD_X11 - KeySym keysym; + KeySym keysym; #endif -}; + }; -constexpr auto UNKNOWN = 0; + constexpr auto UNKNOWN = 0; -/** + /** * @brief Initializes the keycode constants for translating * moonlight keycodes to linux/X11 keycodes */ -static constexpr std::array init_keycodes() { - std::array keycodes {}; + static constexpr std::array + init_keycodes() { + std::array keycodes {}; #ifdef SUNSHINE_BUILD_X11 -#define __CONVERT_UNSAFE(wincode, linuxcode, scancode, keysym) \ - keycodes[wincode] = keycode_t { linuxcode, scancode, keysym }; + #define __CONVERT_UNSAFE(wincode, linuxcode, scancode, keysym) \ + keycodes[wincode] = keycode_t { linuxcode, scancode, keysym }; #else -#define __CONVERT_UNSAFE(wincode, linuxcode, scancode, keysym) \ - keycodes[wincode] = keycode_t { linuxcode, scancode }; + #define __CONVERT_UNSAFE(wincode, linuxcode, scancode, keysym) \ + keycodes[wincode] = keycode_t { linuxcode, scancode }; #endif #define __CONVERT(wincode, linuxcode, scancode, keysym) \ @@ -160,863 +163,890 @@ static constexpr std::array init_keycodes() { static_assert(wincode >= 0, "Are you mad?, keycode needs to be greater than zero"); \ __CONVERT_UNSAFE(wincode, linuxcode, scancode, keysym) - __CONVERT(0x08 /* VKEY_BACK */, KEY_BACKSPACE, 0x7002A, XK_BackSpace); - __CONVERT(0x09 /* VKEY_TAB */, KEY_TAB, 0x7002B, XK_Tab); - __CONVERT(0x0C /* VKEY_CLEAR */, KEY_CLEAR, UNKNOWN, XK_Clear); - __CONVERT(0x0D /* VKEY_RETURN */, KEY_ENTER, 0x70028, XK_Return); - __CONVERT(0x10 /* VKEY_SHIFT */, KEY_LEFTSHIFT, 0x700E1, XK_Shift_L); - __CONVERT(0x11 /* VKEY_CONTROL */, KEY_LEFTCTRL, 0x700E0, XK_Control_L); - __CONVERT(0x12 /* VKEY_MENU */, KEY_LEFTALT, UNKNOWN, XK_Alt_L); - __CONVERT(0x13 /* VKEY_PAUSE */, KEY_PAUSE, UNKNOWN, XK_Pause); - __CONVERT(0x14 /* VKEY_CAPITAL */, KEY_CAPSLOCK, 0x70039, XK_Caps_Lock); - __CONVERT(0x15 /* VKEY_KANA */, KEY_KATAKANAHIRAGANA, UNKNOWN, XK_Kana_Shift); - __CONVERT(0x16 /* VKEY_HANGUL */, KEY_HANGEUL, UNKNOWN, XK_Hangul); - __CONVERT(0x17 /* VKEY_JUNJA */, KEY_HANJA, UNKNOWN, XK_Hangul_Jeonja); - __CONVERT(0x19 /* VKEY_KANJI */, KEY_KATAKANA, UNKNOWN, XK_Kanji); - __CONVERT(0x1B /* VKEY_ESCAPE */, KEY_ESC, 0x70029, XK_Escape); - __CONVERT(0x20 /* VKEY_SPACE */, KEY_SPACE, 0x7002C, XK_space); - __CONVERT(0x21 /* VKEY_PRIOR */, KEY_PAGEUP, 0x7004B, XK_Page_Up); - __CONVERT(0x22 /* VKEY_NEXT */, KEY_PAGEDOWN, 0x7004E, XK_Page_Down); - __CONVERT(0x23 /* VKEY_END */, KEY_END, 0x7004D, XK_End); - __CONVERT(0x24 /* VKEY_HOME */, KEY_HOME, 0x7004A, XK_Home); - __CONVERT(0x25 /* VKEY_LEFT */, KEY_LEFT, 0x70050, XK_Left); - __CONVERT(0x26 /* VKEY_UP */, KEY_UP, 0x70052, XK_Up); - __CONVERT(0x27 /* VKEY_RIGHT */, KEY_RIGHT, 0x7004F, XK_Right); - __CONVERT(0x28 /* VKEY_DOWN */, KEY_DOWN, 0x70051, XK_Down); - __CONVERT(0x29 /* VKEY_SELECT */, KEY_SELECT, UNKNOWN, XK_Select); - __CONVERT(0x2A /* VKEY_PRINT */, KEY_PRINT, UNKNOWN, XK_Print); - __CONVERT(0x2C /* VKEY_SNAPSHOT */, KEY_SYSRQ, 0x70046, XK_Sys_Req); - __CONVERT(0x2D /* VKEY_INSERT */, KEY_INSERT, 0x70049, XK_Insert); - __CONVERT(0x2E /* VKEY_DELETE */, KEY_DELETE, 0x7004C, XK_Delete); - __CONVERT(0x2F /* VKEY_HELP */, KEY_HELP, UNKNOWN, XK_Help); - __CONVERT(0x30 /* VKEY_0 */, KEY_0, 0x70027, XK_0); - __CONVERT(0x31 /* VKEY_1 */, KEY_1, 0x7001E, XK_1); - __CONVERT(0x32 /* VKEY_2 */, KEY_2, 0x7001F, XK_2); - __CONVERT(0x33 /* VKEY_3 */, KEY_3, 0x70020, XK_3); - __CONVERT(0x34 /* VKEY_4 */, KEY_4, 0x70021, XK_4); - __CONVERT(0x35 /* VKEY_5 */, KEY_5, 0x70022, XK_5); - __CONVERT(0x36 /* VKEY_6 */, KEY_6, 0x70023, XK_6); - __CONVERT(0x37 /* VKEY_7 */, KEY_7, 0x70024, XK_7); - __CONVERT(0x38 /* VKEY_8 */, KEY_8, 0x70025, XK_8); - __CONVERT(0x39 /* VKEY_9 */, KEY_9, 0x70026, XK_9); - __CONVERT(0x41 /* VKEY_A */, KEY_A, 0x70004, XK_A); - __CONVERT(0x42 /* VKEY_B */, KEY_B, 0x70005, XK_B); - __CONVERT(0x43 /* VKEY_C */, KEY_C, 0x70006, XK_C); - __CONVERT(0x44 /* VKEY_D */, KEY_D, 0x70007, XK_D); - __CONVERT(0x45 /* VKEY_E */, KEY_E, 0x70008, XK_E); - __CONVERT(0x46 /* VKEY_F */, KEY_F, 0x70009, XK_F); - __CONVERT(0x47 /* VKEY_G */, KEY_G, 0x7000A, XK_G); - __CONVERT(0x48 /* VKEY_H */, KEY_H, 0x7000B, XK_H); - __CONVERT(0x49 /* VKEY_I */, KEY_I, 0x7000C, XK_I); - __CONVERT(0x4A /* VKEY_J */, KEY_J, 0x7000D, XK_J); - __CONVERT(0x4B /* VKEY_K */, KEY_K, 0x7000E, XK_K); - __CONVERT(0x4C /* VKEY_L */, KEY_L, 0x7000F, XK_L); - __CONVERT(0x4D /* VKEY_M */, KEY_M, 0x70010, XK_M); - __CONVERT(0x4E /* VKEY_N */, KEY_N, 0x70011, XK_N); - __CONVERT(0x4F /* VKEY_O */, KEY_O, 0x70012, XK_O); - __CONVERT(0x50 /* VKEY_P */, KEY_P, 0x70013, XK_P); - __CONVERT(0x51 /* VKEY_Q */, KEY_Q, 0x70014, XK_Q); - __CONVERT(0x52 /* VKEY_R */, KEY_R, 0x70015, XK_R); - __CONVERT(0x53 /* VKEY_S */, KEY_S, 0x70016, XK_S); - __CONVERT(0x54 /* VKEY_T */, KEY_T, 0x70017, XK_T); - __CONVERT(0x55 /* VKEY_U */, KEY_U, 0x70018, XK_U); - __CONVERT(0x56 /* VKEY_V */, KEY_V, 0x70019, XK_V); - __CONVERT(0x57 /* VKEY_W */, KEY_W, 0x7001A, XK_W); - __CONVERT(0x58 /* VKEY_X */, KEY_X, 0x7001B, XK_X); - __CONVERT(0x59 /* VKEY_Y */, KEY_Y, 0x7001C, XK_Y); - __CONVERT(0x5A /* VKEY_Z */, KEY_Z, 0x7001D, XK_Z); - __CONVERT(0x5B /* VKEY_LWIN */, KEY_LEFTMETA, 0x700E3, XK_Meta_L); - __CONVERT(0x5C /* VKEY_RWIN */, KEY_RIGHTMETA, 0x700E7, XK_Meta_R); - __CONVERT(0x5F /* VKEY_SLEEP */, KEY_SLEEP, UNKNOWN, UNKNOWN); - __CONVERT(0x60 /* VKEY_NUMPAD0 */, KEY_KP0, 0x70062, XK_KP_0); - __CONVERT(0x61 /* VKEY_NUMPAD1 */, KEY_KP1, 0x70059, XK_KP_1); - __CONVERT(0x62 /* VKEY_NUMPAD2 */, KEY_KP2, 0x7005A, XK_KP_2); - __CONVERT(0x63 /* VKEY_NUMPAD3 */, KEY_KP3, 0x7005B, XK_KP_3); - __CONVERT(0x64 /* VKEY_NUMPAD4 */, KEY_KP4, 0x7005C, XK_KP_4); - __CONVERT(0x65 /* VKEY_NUMPAD5 */, KEY_KP5, 0x7005D, XK_KP_5); - __CONVERT(0x66 /* VKEY_NUMPAD6 */, KEY_KP6, 0x7005E, XK_KP_6); - __CONVERT(0x67 /* VKEY_NUMPAD7 */, KEY_KP7, 0x7005F, XK_KP_7); - __CONVERT(0x68 /* VKEY_NUMPAD8 */, KEY_KP8, 0x70060, XK_KP_8); - __CONVERT(0x69 /* VKEY_NUMPAD9 */, KEY_KP9, 0x70061, XK_KP_9); - __CONVERT(0x6A /* VKEY_MULTIPLY */, KEY_KPASTERISK, 0x70055, XK_KP_Multiply); - __CONVERT(0x6B /* VKEY_ADD */, KEY_KPPLUS, 0x70057, XK_KP_Add); - __CONVERT(0x6C /* VKEY_SEPARATOR */, KEY_KPCOMMA, UNKNOWN, XK_KP_Separator); - __CONVERT(0x6D /* VKEY_SUBTRACT */, KEY_KPMINUS, 0x70056, XK_KP_Subtract); - __CONVERT(0x6E /* VKEY_DECIMAL */, KEY_KPDOT, 0x70063, XK_KP_Decimal); - __CONVERT(0x6F /* VKEY_DIVIDE */, KEY_KPSLASH, 0x70054, XK_KP_Divide); - __CONVERT(0x70 /* VKEY_F1 */, KEY_F1, 0x70046, XK_F1); - __CONVERT(0x71 /* VKEY_F2 */, KEY_F2, 0x70047, XK_F2); - __CONVERT(0x72 /* VKEY_F3 */, KEY_F3, 0x70048, XK_F3); - __CONVERT(0x73 /* VKEY_F4 */, KEY_F4, 0x70049, XK_F4); - __CONVERT(0x74 /* VKEY_F5 */, KEY_F5, 0x7004a, XK_F5); - __CONVERT(0x75 /* VKEY_F6 */, KEY_F6, 0x7004b, XK_F6); - __CONVERT(0x76 /* VKEY_F7 */, KEY_F7, 0x7004c, XK_F7); - __CONVERT(0x77 /* VKEY_F8 */, KEY_F8, 0x7004d, XK_F8); - __CONVERT(0x78 /* VKEY_F9 */, KEY_F9, 0x7004e, XK_F9); - __CONVERT(0x79 /* VKEY_F10 */, KEY_F10, 0x70044, XK_F10); - __CONVERT(0x7A /* VKEY_F11 */, KEY_F11, 0x70044, XK_F11); - __CONVERT(0x7B /* VKEY_F12 */, KEY_F12, 0x70045, XK_F12); - __CONVERT(0x7C /* VKEY_F13 */, KEY_F13, 0x7003a, XK_F13); - __CONVERT(0x7D /* VKEY_F14 */, KEY_F14, 0x7003b, XK_F14); - __CONVERT(0x7E /* VKEY_F15 */, KEY_F15, 0x7003c, XK_F15); - __CONVERT(0x7F /* VKEY_F16 */, KEY_F16, 0x7003d, XK_F16); - __CONVERT(0x80 /* VKEY_F17 */, KEY_F17, 0x7003e, XK_F17); - __CONVERT(0x81 /* VKEY_F18 */, KEY_F18, 0x7003f, XK_F18); - __CONVERT(0x82 /* VKEY_F19 */, KEY_F19, 0x70040, XK_F19); - __CONVERT(0x83 /* VKEY_F20 */, KEY_F20, 0x70041, XK_F20); - __CONVERT(0x84 /* VKEY_F21 */, KEY_F21, 0x70042, XK_F21); - __CONVERT(0x85 /* VKEY_F22 */, KEY_F12, 0x70043, XK_F12); - __CONVERT(0x86 /* VKEY_F23 */, KEY_F23, 0x70044, XK_F23); - __CONVERT(0x87 /* VKEY_F24 */, KEY_F24, 0x70045, XK_F24); - __CONVERT(0x90 /* VKEY_NUMLOCK */, KEY_NUMLOCK, 0x70053, XK_Num_Lock); - __CONVERT(0x91 /* VKEY_SCROLL */, KEY_SCROLLLOCK, 0x70047, XK_Scroll_Lock); - __CONVERT(0xA0 /* VKEY_LSHIFT */, KEY_LEFTSHIFT, 0x700E1, XK_Shift_L); - __CONVERT(0xA1 /* VKEY_RSHIFT */, KEY_RIGHTSHIFT, 0x700E5, XK_Shift_R); - __CONVERT(0xA2 /* VKEY_LCONTROL */, KEY_LEFTCTRL, 0x700E0, XK_Control_L); - __CONVERT(0xA3 /* VKEY_RCONTROL */, KEY_RIGHTCTRL, 0x700E4, XK_Control_R); - __CONVERT(0xA4 /* VKEY_LMENU */, KEY_LEFTALT, 0x7002E, XK_Alt_L); - __CONVERT(0xA5 /* VKEY_RMENU */, KEY_RIGHTALT, 0x700E6, XK_Alt_R); - __CONVERT(0xBA /* VKEY_OEM_1 */, KEY_SEMICOLON, 0x70033, XK_semicolon); - __CONVERT(0xBB /* VKEY_OEM_PLUS */, KEY_EQUAL, 0x7002E, XK_equal); - __CONVERT(0xBC /* VKEY_OEM_COMMA */, KEY_COMMA, 0x70036, XK_comma); - __CONVERT(0xBD /* VKEY_OEM_MINUS */, KEY_MINUS, 0x7002D, XK_minus); - __CONVERT(0xBE /* VKEY_OEM_PERIOD */, KEY_DOT, 0x70037, XK_period); - __CONVERT(0xBF /* VKEY_OEM_2 */, KEY_SLASH, 0x70038, XK_slash); - __CONVERT(0xC0 /* VKEY_OEM_3 */, KEY_GRAVE, 0x70035, XK_grave); - __CONVERT(0xDB /* VKEY_OEM_4 */, KEY_LEFTBRACE, 0x7002F, XK_braceleft); - __CONVERT(0xDC /* VKEY_OEM_5 */, KEY_BACKSLASH, 0x70031, XK_backslash); - __CONVERT(0xDD /* VKEY_OEM_6 */, KEY_RIGHTBRACE, 0x70030, XK_braceright); - __CONVERT(0xDE /* VKEY_OEM_7 */, KEY_APOSTROPHE, 0x70034, XK_apostrophe); - __CONVERT(0xE2 /* VKEY_NON_US_BACKSLASH */, KEY_102ND, 0x70064, XK_backslash); + __CONVERT(0x08 /* VKEY_BACK */, KEY_BACKSPACE, 0x7002A, XK_BackSpace); + __CONVERT(0x09 /* VKEY_TAB */, KEY_TAB, 0x7002B, XK_Tab); + __CONVERT(0x0C /* VKEY_CLEAR */, KEY_CLEAR, UNKNOWN, XK_Clear); + __CONVERT(0x0D /* VKEY_RETURN */, KEY_ENTER, 0x70028, XK_Return); + __CONVERT(0x10 /* VKEY_SHIFT */, KEY_LEFTSHIFT, 0x700E1, XK_Shift_L); + __CONVERT(0x11 /* VKEY_CONTROL */, KEY_LEFTCTRL, 0x700E0, XK_Control_L); + __CONVERT(0x12 /* VKEY_MENU */, KEY_LEFTALT, UNKNOWN, XK_Alt_L); + __CONVERT(0x13 /* VKEY_PAUSE */, KEY_PAUSE, UNKNOWN, XK_Pause); + __CONVERT(0x14 /* VKEY_CAPITAL */, KEY_CAPSLOCK, 0x70039, XK_Caps_Lock); + __CONVERT(0x15 /* VKEY_KANA */, KEY_KATAKANAHIRAGANA, UNKNOWN, XK_Kana_Shift); + __CONVERT(0x16 /* VKEY_HANGUL */, KEY_HANGEUL, UNKNOWN, XK_Hangul); + __CONVERT(0x17 /* VKEY_JUNJA */, KEY_HANJA, UNKNOWN, XK_Hangul_Jeonja); + __CONVERT(0x19 /* VKEY_KANJI */, KEY_KATAKANA, UNKNOWN, XK_Kanji); + __CONVERT(0x1B /* VKEY_ESCAPE */, KEY_ESC, 0x70029, XK_Escape); + __CONVERT(0x20 /* VKEY_SPACE */, KEY_SPACE, 0x7002C, XK_space); + __CONVERT(0x21 /* VKEY_PRIOR */, KEY_PAGEUP, 0x7004B, XK_Page_Up); + __CONVERT(0x22 /* VKEY_NEXT */, KEY_PAGEDOWN, 0x7004E, XK_Page_Down); + __CONVERT(0x23 /* VKEY_END */, KEY_END, 0x7004D, XK_End); + __CONVERT(0x24 /* VKEY_HOME */, KEY_HOME, 0x7004A, XK_Home); + __CONVERT(0x25 /* VKEY_LEFT */, KEY_LEFT, 0x70050, XK_Left); + __CONVERT(0x26 /* VKEY_UP */, KEY_UP, 0x70052, XK_Up); + __CONVERT(0x27 /* VKEY_RIGHT */, KEY_RIGHT, 0x7004F, XK_Right); + __CONVERT(0x28 /* VKEY_DOWN */, KEY_DOWN, 0x70051, XK_Down); + __CONVERT(0x29 /* VKEY_SELECT */, KEY_SELECT, UNKNOWN, XK_Select); + __CONVERT(0x2A /* VKEY_PRINT */, KEY_PRINT, UNKNOWN, XK_Print); + __CONVERT(0x2C /* VKEY_SNAPSHOT */, KEY_SYSRQ, 0x70046, XK_Sys_Req); + __CONVERT(0x2D /* VKEY_INSERT */, KEY_INSERT, 0x70049, XK_Insert); + __CONVERT(0x2E /* VKEY_DELETE */, KEY_DELETE, 0x7004C, XK_Delete); + __CONVERT(0x2F /* VKEY_HELP */, KEY_HELP, UNKNOWN, XK_Help); + __CONVERT(0x30 /* VKEY_0 */, KEY_0, 0x70027, XK_0); + __CONVERT(0x31 /* VKEY_1 */, KEY_1, 0x7001E, XK_1); + __CONVERT(0x32 /* VKEY_2 */, KEY_2, 0x7001F, XK_2); + __CONVERT(0x33 /* VKEY_3 */, KEY_3, 0x70020, XK_3); + __CONVERT(0x34 /* VKEY_4 */, KEY_4, 0x70021, XK_4); + __CONVERT(0x35 /* VKEY_5 */, KEY_5, 0x70022, XK_5); + __CONVERT(0x36 /* VKEY_6 */, KEY_6, 0x70023, XK_6); + __CONVERT(0x37 /* VKEY_7 */, KEY_7, 0x70024, XK_7); + __CONVERT(0x38 /* VKEY_8 */, KEY_8, 0x70025, XK_8); + __CONVERT(0x39 /* VKEY_9 */, KEY_9, 0x70026, XK_9); + __CONVERT(0x41 /* VKEY_A */, KEY_A, 0x70004, XK_A); + __CONVERT(0x42 /* VKEY_B */, KEY_B, 0x70005, XK_B); + __CONVERT(0x43 /* VKEY_C */, KEY_C, 0x70006, XK_C); + __CONVERT(0x44 /* VKEY_D */, KEY_D, 0x70007, XK_D); + __CONVERT(0x45 /* VKEY_E */, KEY_E, 0x70008, XK_E); + __CONVERT(0x46 /* VKEY_F */, KEY_F, 0x70009, XK_F); + __CONVERT(0x47 /* VKEY_G */, KEY_G, 0x7000A, XK_G); + __CONVERT(0x48 /* VKEY_H */, KEY_H, 0x7000B, XK_H); + __CONVERT(0x49 /* VKEY_I */, KEY_I, 0x7000C, XK_I); + __CONVERT(0x4A /* VKEY_J */, KEY_J, 0x7000D, XK_J); + __CONVERT(0x4B /* VKEY_K */, KEY_K, 0x7000E, XK_K); + __CONVERT(0x4C /* VKEY_L */, KEY_L, 0x7000F, XK_L); + __CONVERT(0x4D /* VKEY_M */, KEY_M, 0x70010, XK_M); + __CONVERT(0x4E /* VKEY_N */, KEY_N, 0x70011, XK_N); + __CONVERT(0x4F /* VKEY_O */, KEY_O, 0x70012, XK_O); + __CONVERT(0x50 /* VKEY_P */, KEY_P, 0x70013, XK_P); + __CONVERT(0x51 /* VKEY_Q */, KEY_Q, 0x70014, XK_Q); + __CONVERT(0x52 /* VKEY_R */, KEY_R, 0x70015, XK_R); + __CONVERT(0x53 /* VKEY_S */, KEY_S, 0x70016, XK_S); + __CONVERT(0x54 /* VKEY_T */, KEY_T, 0x70017, XK_T); + __CONVERT(0x55 /* VKEY_U */, KEY_U, 0x70018, XK_U); + __CONVERT(0x56 /* VKEY_V */, KEY_V, 0x70019, XK_V); + __CONVERT(0x57 /* VKEY_W */, KEY_W, 0x7001A, XK_W); + __CONVERT(0x58 /* VKEY_X */, KEY_X, 0x7001B, XK_X); + __CONVERT(0x59 /* VKEY_Y */, KEY_Y, 0x7001C, XK_Y); + __CONVERT(0x5A /* VKEY_Z */, KEY_Z, 0x7001D, XK_Z); + __CONVERT(0x5B /* VKEY_LWIN */, KEY_LEFTMETA, 0x700E3, XK_Meta_L); + __CONVERT(0x5C /* VKEY_RWIN */, KEY_RIGHTMETA, 0x700E7, XK_Meta_R); + __CONVERT(0x5F /* VKEY_SLEEP */, KEY_SLEEP, UNKNOWN, UNKNOWN); + __CONVERT(0x60 /* VKEY_NUMPAD0 */, KEY_KP0, 0x70062, XK_KP_0); + __CONVERT(0x61 /* VKEY_NUMPAD1 */, KEY_KP1, 0x70059, XK_KP_1); + __CONVERT(0x62 /* VKEY_NUMPAD2 */, KEY_KP2, 0x7005A, XK_KP_2); + __CONVERT(0x63 /* VKEY_NUMPAD3 */, KEY_KP3, 0x7005B, XK_KP_3); + __CONVERT(0x64 /* VKEY_NUMPAD4 */, KEY_KP4, 0x7005C, XK_KP_4); + __CONVERT(0x65 /* VKEY_NUMPAD5 */, KEY_KP5, 0x7005D, XK_KP_5); + __CONVERT(0x66 /* VKEY_NUMPAD6 */, KEY_KP6, 0x7005E, XK_KP_6); + __CONVERT(0x67 /* VKEY_NUMPAD7 */, KEY_KP7, 0x7005F, XK_KP_7); + __CONVERT(0x68 /* VKEY_NUMPAD8 */, KEY_KP8, 0x70060, XK_KP_8); + __CONVERT(0x69 /* VKEY_NUMPAD9 */, KEY_KP9, 0x70061, XK_KP_9); + __CONVERT(0x6A /* VKEY_MULTIPLY */, KEY_KPASTERISK, 0x70055, XK_KP_Multiply); + __CONVERT(0x6B /* VKEY_ADD */, KEY_KPPLUS, 0x70057, XK_KP_Add); + __CONVERT(0x6C /* VKEY_SEPARATOR */, KEY_KPCOMMA, UNKNOWN, XK_KP_Separator); + __CONVERT(0x6D /* VKEY_SUBTRACT */, KEY_KPMINUS, 0x70056, XK_KP_Subtract); + __CONVERT(0x6E /* VKEY_DECIMAL */, KEY_KPDOT, 0x70063, XK_KP_Decimal); + __CONVERT(0x6F /* VKEY_DIVIDE */, KEY_KPSLASH, 0x70054, XK_KP_Divide); + __CONVERT(0x70 /* VKEY_F1 */, KEY_F1, 0x70046, XK_F1); + __CONVERT(0x71 /* VKEY_F2 */, KEY_F2, 0x70047, XK_F2); + __CONVERT(0x72 /* VKEY_F3 */, KEY_F3, 0x70048, XK_F3); + __CONVERT(0x73 /* VKEY_F4 */, KEY_F4, 0x70049, XK_F4); + __CONVERT(0x74 /* VKEY_F5 */, KEY_F5, 0x7004a, XK_F5); + __CONVERT(0x75 /* VKEY_F6 */, KEY_F6, 0x7004b, XK_F6); + __CONVERT(0x76 /* VKEY_F7 */, KEY_F7, 0x7004c, XK_F7); + __CONVERT(0x77 /* VKEY_F8 */, KEY_F8, 0x7004d, XK_F8); + __CONVERT(0x78 /* VKEY_F9 */, KEY_F9, 0x7004e, XK_F9); + __CONVERT(0x79 /* VKEY_F10 */, KEY_F10, 0x70044, XK_F10); + __CONVERT(0x7A /* VKEY_F11 */, KEY_F11, 0x70044, XK_F11); + __CONVERT(0x7B /* VKEY_F12 */, KEY_F12, 0x70045, XK_F12); + __CONVERT(0x7C /* VKEY_F13 */, KEY_F13, 0x7003a, XK_F13); + __CONVERT(0x7D /* VKEY_F14 */, KEY_F14, 0x7003b, XK_F14); + __CONVERT(0x7E /* VKEY_F15 */, KEY_F15, 0x7003c, XK_F15); + __CONVERT(0x7F /* VKEY_F16 */, KEY_F16, 0x7003d, XK_F16); + __CONVERT(0x80 /* VKEY_F17 */, KEY_F17, 0x7003e, XK_F17); + __CONVERT(0x81 /* VKEY_F18 */, KEY_F18, 0x7003f, XK_F18); + __CONVERT(0x82 /* VKEY_F19 */, KEY_F19, 0x70040, XK_F19); + __CONVERT(0x83 /* VKEY_F20 */, KEY_F20, 0x70041, XK_F20); + __CONVERT(0x84 /* VKEY_F21 */, KEY_F21, 0x70042, XK_F21); + __CONVERT(0x85 /* VKEY_F22 */, KEY_F12, 0x70043, XK_F12); + __CONVERT(0x86 /* VKEY_F23 */, KEY_F23, 0x70044, XK_F23); + __CONVERT(0x87 /* VKEY_F24 */, KEY_F24, 0x70045, XK_F24); + __CONVERT(0x90 /* VKEY_NUMLOCK */, KEY_NUMLOCK, 0x70053, XK_Num_Lock); + __CONVERT(0x91 /* VKEY_SCROLL */, KEY_SCROLLLOCK, 0x70047, XK_Scroll_Lock); + __CONVERT(0xA0 /* VKEY_LSHIFT */, KEY_LEFTSHIFT, 0x700E1, XK_Shift_L); + __CONVERT(0xA1 /* VKEY_RSHIFT */, KEY_RIGHTSHIFT, 0x700E5, XK_Shift_R); + __CONVERT(0xA2 /* VKEY_LCONTROL */, KEY_LEFTCTRL, 0x700E0, XK_Control_L); + __CONVERT(0xA3 /* VKEY_RCONTROL */, KEY_RIGHTCTRL, 0x700E4, XK_Control_R); + __CONVERT(0xA4 /* VKEY_LMENU */, KEY_LEFTALT, 0x7002E, XK_Alt_L); + __CONVERT(0xA5 /* VKEY_RMENU */, KEY_RIGHTALT, 0x700E6, XK_Alt_R); + __CONVERT(0xBA /* VKEY_OEM_1 */, KEY_SEMICOLON, 0x70033, XK_semicolon); + __CONVERT(0xBB /* VKEY_OEM_PLUS */, KEY_EQUAL, 0x7002E, XK_equal); + __CONVERT(0xBC /* VKEY_OEM_COMMA */, KEY_COMMA, 0x70036, XK_comma); + __CONVERT(0xBD /* VKEY_OEM_MINUS */, KEY_MINUS, 0x7002D, XK_minus); + __CONVERT(0xBE /* VKEY_OEM_PERIOD */, KEY_DOT, 0x70037, XK_period); + __CONVERT(0xBF /* VKEY_OEM_2 */, KEY_SLASH, 0x70038, XK_slash); + __CONVERT(0xC0 /* VKEY_OEM_3 */, KEY_GRAVE, 0x70035, XK_grave); + __CONVERT(0xDB /* VKEY_OEM_4 */, KEY_LEFTBRACE, 0x7002F, XK_braceleft); + __CONVERT(0xDC /* VKEY_OEM_5 */, KEY_BACKSLASH, 0x70031, XK_backslash); + __CONVERT(0xDD /* VKEY_OEM_6 */, KEY_RIGHTBRACE, 0x70030, XK_braceright); + __CONVERT(0xDE /* VKEY_OEM_7 */, KEY_APOSTROPHE, 0x70034, XK_apostrophe); + __CONVERT(0xE2 /* VKEY_NON_US_BACKSLASH */, KEY_102ND, 0x70064, XK_backslash); #undef __CONVERT #undef __CONVERT_UNSAFE - return keycodes; -} - -static constexpr auto keycodes = init_keycodes(); - -constexpr touch_port_t target_touch_port { - 0, 0, - 19200, 12000 -}; - -static std::pair operator*(const std::pair &l, int r) { - return { - l.first * r, - l.second * r, - }; -} - -static std::pair operator/(const std::pair &l, int r) { - return { - l.first / r, - l.second / r, - }; -} - -static std::pair &operator+=(std::pair &l, const std::pair &r) { - l.first += r.first; - l.second += r.second; - - return l; -} - -static inline void print(const ff_envelope &envelope) { - BOOST_LOG(debug) - << "Envelope:"sv << std::endl - << " attack_length: " << envelope.attack_length << std::endl - << " attack_level: " << envelope.attack_level << std::endl - << " fade_length: " << envelope.fade_length << std::endl - << " fade_level: " << envelope.fade_level; -} - -static inline void print(const ff_replay &replay) { - BOOST_LOG(debug) - << "Replay:"sv << std::endl - << " length: "sv << replay.length << std::endl - << " delay: "sv << replay.delay; -} - -static inline void print(const ff_trigger &trigger) { - BOOST_LOG(debug) - << "Trigger:"sv << std::endl - << " button: "sv << trigger.button << std::endl - << " interval: "sv << trigger.interval; -} - -static inline void print(const ff_effect &effect) { - BOOST_LOG(debug) - << std::endl - << std::endl - << "Received rumble effect with id: ["sv << effect.id << ']'; - - switch(effect.type) { - case FF_CONSTANT: - BOOST_LOG(debug) - << "FF_CONSTANT:"sv << std::endl - << " direction: "sv << effect.direction << std::endl - << " level: "sv << effect.u.constant.level; - - print(effect.u.constant.envelope); - break; - - case FF_PERIODIC: - BOOST_LOG(debug) - << "FF_CONSTANT:"sv << std::endl - << " direction: "sv << effect.direction << std::endl - << " waveform: "sv << effect.u.periodic.waveform << std::endl - << " period: "sv << effect.u.periodic.period << std::endl - << " magnitude: "sv << effect.u.periodic.magnitude << std::endl - << " offset: "sv << effect.u.periodic.offset << std::endl - << " phase: "sv << effect.u.periodic.phase; - - print(effect.u.periodic.envelope); - break; - - case FF_RAMP: - BOOST_LOG(debug) - << "FF_RAMP:"sv << std::endl - << " direction: "sv << effect.direction << std::endl - << " start_level:" << effect.u.ramp.start_level << std::endl - << " end_level:" << effect.u.ramp.end_level; - - print(effect.u.ramp.envelope); - break; - - case FF_RUMBLE: - BOOST_LOG(debug) - << "FF_RUMBLE:" << std::endl - << " direction: "sv << effect.direction << std::endl - << " strong_magnitude: " << effect.u.rumble.strong_magnitude << std::endl - << " weak_magnitude: " << effect.u.rumble.weak_magnitude; - break; - - - case FF_SPRING: - BOOST_LOG(debug) - << "FF_SPRING:" << std::endl - << " direction: "sv << effect.direction; - break; - - case FF_FRICTION: - BOOST_LOG(debug) - << "FF_FRICTION:" << std::endl - << " direction: "sv << effect.direction; - break; - - case FF_DAMPER: - BOOST_LOG(debug) - << "FF_DAMPER:" << std::endl - << " direction: "sv << effect.direction; - break; - - case FF_INERTIA: - BOOST_LOG(debug) - << "FF_INERTIA:" << std::endl - << " direction: "sv << effect.direction; - break; - - case FF_CUSTOM: - BOOST_LOG(debug) - << "FF_CUSTOM:" << std::endl - << " direction: "sv << effect.direction; - break; - - default: - BOOST_LOG(debug) - << "FF_UNKNOWN:" << std::endl - << " direction: "sv << effect.direction; - break; + return keycodes; } - print(effect.replay); - print(effect.trigger); -} + static constexpr auto keycodes = init_keycodes(); -// Emulate rumble effects -class effect_t { -public: - KITTY_DEFAULT_CONSTR_MOVE(effect_t) + constexpr touch_port_t target_touch_port { + 0, 0, + 19200, 12000 + }; - effect_t(int gamepadnr, uinput_t::pointer dev, rumble_queue_t &&q) - : gamepadnr { gamepadnr }, dev { dev }, rumble_queue { std::move(q) }, gain { 0xFFFF }, id_to_data {} {} + static std::pair + operator*(const std::pair &l, int r) { + return { + l.first * r, + l.second * r, + }; + } - class data_t { + static std::pair + operator/(const std::pair &l, int r) { + return { + l.first / r, + l.second / r, + }; + } + + static std::pair & + operator+=(std::pair &l, const std::pair &r) { + l.first += r.first; + l.second += r.second; + + return l; + } + + static inline void + print(const ff_envelope &envelope) { + BOOST_LOG(debug) + << "Envelope:"sv << std::endl + << " attack_length: " << envelope.attack_length << std::endl + << " attack_level: " << envelope.attack_level << std::endl + << " fade_length: " << envelope.fade_length << std::endl + << " fade_level: " << envelope.fade_level; + } + + static inline void + print(const ff_replay &replay) { + BOOST_LOG(debug) + << "Replay:"sv << std::endl + << " length: "sv << replay.length << std::endl + << " delay: "sv << replay.delay; + } + + static inline void + print(const ff_trigger &trigger) { + BOOST_LOG(debug) + << "Trigger:"sv << std::endl + << " button: "sv << trigger.button << std::endl + << " interval: "sv << trigger.interval; + } + + static inline void + print(const ff_effect &effect) { + BOOST_LOG(debug) + << std::endl + << std::endl + << "Received rumble effect with id: ["sv << effect.id << ']'; + + switch (effect.type) { + case FF_CONSTANT: + BOOST_LOG(debug) + << "FF_CONSTANT:"sv << std::endl + << " direction: "sv << effect.direction << std::endl + << " level: "sv << effect.u.constant.level; + + print(effect.u.constant.envelope); + break; + + case FF_PERIODIC: + BOOST_LOG(debug) + << "FF_CONSTANT:"sv << std::endl + << " direction: "sv << effect.direction << std::endl + << " waveform: "sv << effect.u.periodic.waveform << std::endl + << " period: "sv << effect.u.periodic.period << std::endl + << " magnitude: "sv << effect.u.periodic.magnitude << std::endl + << " offset: "sv << effect.u.periodic.offset << std::endl + << " phase: "sv << effect.u.periodic.phase; + + print(effect.u.periodic.envelope); + break; + + case FF_RAMP: + BOOST_LOG(debug) + << "FF_RAMP:"sv << std::endl + << " direction: "sv << effect.direction << std::endl + << " start_level:" << effect.u.ramp.start_level << std::endl + << " end_level:" << effect.u.ramp.end_level; + + print(effect.u.ramp.envelope); + break; + + case FF_RUMBLE: + BOOST_LOG(debug) + << "FF_RUMBLE:" << std::endl + << " direction: "sv << effect.direction << std::endl + << " strong_magnitude: " << effect.u.rumble.strong_magnitude << std::endl + << " weak_magnitude: " << effect.u.rumble.weak_magnitude; + break; + + case FF_SPRING: + BOOST_LOG(debug) + << "FF_SPRING:" << std::endl + << " direction: "sv << effect.direction; + break; + + case FF_FRICTION: + BOOST_LOG(debug) + << "FF_FRICTION:" << std::endl + << " direction: "sv << effect.direction; + break; + + case FF_DAMPER: + BOOST_LOG(debug) + << "FF_DAMPER:" << std::endl + << " direction: "sv << effect.direction; + break; + + case FF_INERTIA: + BOOST_LOG(debug) + << "FF_INERTIA:" << std::endl + << " direction: "sv << effect.direction; + break; + + case FF_CUSTOM: + BOOST_LOG(debug) + << "FF_CUSTOM:" << std::endl + << " direction: "sv << effect.direction; + break; + + default: + BOOST_LOG(debug) + << "FF_UNKNOWN:" << std::endl + << " direction: "sv << effect.direction; + break; + } + + print(effect.replay); + print(effect.trigger); + } + + // Emulate rumble effects + class effect_t { public: - KITTY_DEFAULT_CONSTR(data_t) + KITTY_DEFAULT_CONSTR_MOVE(effect_t) - data_t(const ff_effect &effect) - : delay { effect.replay.delay }, + effect_t(int gamepadnr, uinput_t::pointer dev, rumble_queue_t &&q): + gamepadnr { gamepadnr }, dev { dev }, rumble_queue { std::move(q) }, gain { 0xFFFF }, id_to_data {} {} + + class data_t { + public: + KITTY_DEFAULT_CONSTR(data_t) + + data_t(const ff_effect &effect): + delay { effect.replay.delay }, length { effect.replay.length }, end_point { std::chrono::steady_clock::time_point::min() }, envelope {}, start {}, end {} { + switch (effect.type) { + case FF_CONSTANT: + start.weak = effect.u.constant.level; + start.strong = effect.u.constant.level; + end.weak = effect.u.constant.level; + end.strong = effect.u.constant.level; - switch(effect.type) { - case FF_CONSTANT: - start.weak = effect.u.constant.level; - start.strong = effect.u.constant.level; - end.weak = effect.u.constant.level; - end.strong = effect.u.constant.level; + envelope = effect.u.constant.envelope; + break; + case FF_PERIODIC: + start.weak = effect.u.periodic.magnitude; + start.strong = effect.u.periodic.magnitude; + end.weak = effect.u.periodic.magnitude; + end.strong = effect.u.periodic.magnitude; - envelope = effect.u.constant.envelope; - break; - case FF_PERIODIC: - start.weak = effect.u.periodic.magnitude; - start.strong = effect.u.periodic.magnitude; - end.weak = effect.u.periodic.magnitude; - end.strong = effect.u.periodic.magnitude; + envelope = effect.u.periodic.envelope; + break; - envelope = effect.u.periodic.envelope; - break; + case FF_RAMP: + start.weak = effect.u.ramp.start_level; + start.strong = effect.u.ramp.start_level; + end.weak = effect.u.ramp.end_level; + end.strong = effect.u.ramp.end_level; - case FF_RAMP: - start.weak = effect.u.ramp.start_level; - start.strong = effect.u.ramp.start_level; - end.weak = effect.u.ramp.end_level; - end.strong = effect.u.ramp.end_level; + envelope = effect.u.ramp.envelope; + break; - envelope = effect.u.ramp.envelope; - break; + case FF_RUMBLE: + start.weak = effect.u.rumble.weak_magnitude; + start.strong = effect.u.rumble.strong_magnitude; + end.weak = effect.u.rumble.weak_magnitude; + end.strong = effect.u.rumble.strong_magnitude; + break; - case FF_RUMBLE: - start.weak = effect.u.rumble.weak_magnitude; - start.strong = effect.u.rumble.strong_magnitude; - end.weak = effect.u.rumble.weak_magnitude; - end.strong = effect.u.rumble.strong_magnitude; - break; - - default: - BOOST_LOG(warning) << "Effect type ["sv << effect.id << "] not implemented"sv; - } - } - - std::uint32_t magnitude(std::chrono::milliseconds time_left, std::uint32_t start, std::uint32_t end) { - auto rel = end - start; - - return start + (rel * time_left.count() / length.count()); - } - - std::pair rumble(std::chrono::steady_clock::time_point tp) { - if(end_point < tp) { - return {}; + default: + BOOST_LOG(warning) << "Effect type ["sv << effect.id << "] not implemented"sv; + } } - auto time_left = - std::chrono::duration_cast( - end_point - tp); + std::uint32_t + magnitude(std::chrono::milliseconds time_left, std::uint32_t start, std::uint32_t end) { + auto rel = end - start; - // If it needs to be delayed' - if(time_left > length) { - return {}; + return start + (rel * time_left.count() / length.count()); } - auto t = length - time_left; - - auto weak = magnitude(t, start.weak, end.weak); - auto strong = magnitude(t, start.strong, end.strong); - - if(t.count() < envelope.attack_length) { - weak = (envelope.attack_level * t.count() + weak * (envelope.attack_length - t.count())) / envelope.attack_length; - strong = (envelope.attack_level * t.count() + strong * (envelope.attack_length - t.count())) / envelope.attack_length; - } - else if(time_left.count() < envelope.fade_length) { - auto dt = (t - length).count() + envelope.fade_length; - - weak = (envelope.fade_level * dt + weak * (envelope.fade_length - dt)) / envelope.fade_length; - strong = (envelope.fade_level * dt + strong * (envelope.fade_length - dt)) / envelope.fade_length; - } - - return { - weak, strong - }; - } - - void activate() { - end_point = std::chrono::steady_clock::now() + delay + length; - } - - void deactivate() { - end_point = std::chrono::steady_clock::time_point::min(); - } - - std::chrono::milliseconds delay; - std::chrono::milliseconds length; - - std::chrono::steady_clock::time_point end_point; - - ff_envelope envelope; - struct { - std::uint32_t weak, strong; - } start; - - struct { - std::uint32_t weak, strong; - } end; - }; - - std::pair rumble(std::chrono::steady_clock::time_point tp) { - std::pair weak_strong {}; - for(auto &[_, data] : id_to_data) { - weak_strong += data.rumble(tp); - } - - std::clamp(weak_strong.first, 0, 0xFFFF); - std::clamp(weak_strong.second, 0, 0xFFFF); - - old_rumble = weak_strong * gain / 0xFFFF; - return old_rumble; - } - - void upload(const ff_effect &effect) { - print(effect); - - auto it = id_to_data.find(effect.id); - - if(it == std::end(id_to_data)) { - id_to_data.emplace(effect.id, effect); - return; - } - - data_t data { effect }; - - data.end_point = it->second.end_point; - it->second = data; - } - - void activate(int id) { - auto it = id_to_data.find(id); - - if(it != std::end(id_to_data)) { - it->second.activate(); - } - } - - void deactivate(int id) { - auto it = id_to_data.find(id); - - if(it != std::end(id_to_data)) { - it->second.deactivate(); - } - } - - void erase(int id) { - id_to_data.erase(id); - BOOST_LOG(debug) << "Removed rumble effect id ["sv << id << ']'; - } - - // Used as ID for rumble notifications - int gamepadnr; - - // Used as ID for adding/removinf devices from evdev notifications - uinput_t::pointer dev; - - rumble_queue_t rumble_queue; - - int gain; - - // No need to send rumble data when old values equals the new values - std::pair old_rumble; - - std::unordered_map id_to_data; -}; - -struct rumble_ctx_t { - std::thread rumble_thread; - - safe::queue_t rumble_queue_queue; -}; - -void broadcastRumble(safe::queue_t &ctx); -int startRumble(rumble_ctx_t &ctx) { - ctx.rumble_thread = std::thread { broadcastRumble, std::ref(ctx.rumble_queue_queue) }; - - return 0; -} - -void stopRumble(rumble_ctx_t &ctx) { - ctx.rumble_queue_queue.stop(); - - BOOST_LOG(debug) << "Waiting for Gamepad notifications to stop..."sv; - ctx.rumble_thread.join(); - BOOST_LOG(debug) << "Gamepad notifications stopped"sv; -} - -static auto notifications = safe::make_shared(startRumble, stopRumble); - -struct input_raw_t { -public: - void clear_touchscreen() { - std::filesystem::path touch_path { appdata() / "sunshine_touchscreen"sv }; - - if(std::filesystem::is_symlink(touch_path)) { - std::filesystem::remove(touch_path); - } - - touch_input.reset(); - } - - void clear_keyboard() { - std::filesystem::path key_path { appdata() / "sunshine_keyboard"sv }; - - if(std::filesystem::is_symlink(key_path)) { - std::filesystem::remove(key_path); - } - - keyboard_input.reset(); - } - - void clear_mouse() { - std::filesystem::path mouse_path { appdata() / "sunshine_mouse"sv }; - - if(std::filesystem::is_symlink(mouse_path)) { - std::filesystem::remove(mouse_path); - } - - mouse_input.reset(); - } - - void clear_gamepad(int nr) { - auto &[dev, _] = gamepads[nr]; - - if(!dev) { - return; - } - - // Remove this gamepad from notifications - rumble_ctx->rumble_queue_queue.raise(nr, dev.get(), nullptr, pollfd_t {}); - - std::stringstream ss; - - ss << "sunshine_gamepad_"sv << nr; - - auto gamepad_path = platf::appdata() / ss.str(); - if(std::filesystem::is_symlink(gamepad_path)) { - std::filesystem::remove(gamepad_path); - } - - gamepads[nr] = std::make_pair(uinput_t {}, gamepad_state_t {}); - } - - int create_mouse() { - int err = libevdev_uinput_create_from_device(mouse_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &mouse_input); - - if(err) { - BOOST_LOG(error) << "Could not create Sunshine Mouse: "sv << strerror(-err); - return -1; - } - - std::filesystem::create_symlink(libevdev_uinput_get_devnode(mouse_input.get()), appdata() / "sunshine_mouse"sv); - - return 0; - } - - int create_touchscreen() { - int err = libevdev_uinput_create_from_device(touch_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &touch_input); - - if(err) { - BOOST_LOG(error) << "Could not create Sunshine Touchscreen: "sv << strerror(-err); - return -1; - } - - std::filesystem::create_symlink(libevdev_uinput_get_devnode(touch_input.get()), appdata() / "sunshine_touchscreen"sv); - - return 0; - } - - int create_keyboard() { - int err = libevdev_uinput_create_from_device(keyboard_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &keyboard_input); - - if(err) { - BOOST_LOG(error) << "Could not create Sunshine Keyboard: "sv << strerror(-err); - return -1; - } - - std::filesystem::create_symlink(libevdev_uinput_get_devnode(keyboard_input.get()), appdata() / "sunshine_keyboard"sv); - - return 0; - } - - int alloc_gamepad(int nr, rumble_queue_t &&rumble_queue) { - TUPLE_2D_REF(input, gamepad_state, gamepads[nr]); - - int err = libevdev_uinput_create_from_device(gamepad_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &input); - - gamepad_state = gamepad_state_t {}; - - if(err) { - BOOST_LOG(error) << "Could not create Sunshine Gamepad: "sv << strerror(-err); - return -1; - } - - std::stringstream ss; - ss << "sunshine_gamepad_"sv << nr; - auto gamepad_path = platf::appdata() / ss.str(); - - if(std::filesystem::is_symlink(gamepad_path)) { - std::filesystem::remove(gamepad_path); - } - - auto dev_node = libevdev_uinput_get_devnode(input.get()); - - rumble_ctx->rumble_queue_queue.raise( - nr, - input.get(), - std::move(rumble_queue), - pollfd_t { - dup(libevdev_uinput_get_fd(input.get())), - (std::int16_t)POLLIN, - (std::int16_t)0, - }); - - std::filesystem::create_symlink(dev_node, gamepad_path); - return 0; - } - - void clear() { - clear_touchscreen(); - clear_keyboard(); - clear_mouse(); - for(int x = 0; x < gamepads.size(); ++x) { - clear_gamepad(x); - } - -#ifdef SUNSHINE_BUILD_X11 - if(display) { - x11::CloseDisplay(display); - display = nullptr; - } -#endif - } - - ~input_raw_t() { - clear(); - } - - safe::shared_t::ptr_t rumble_ctx; - - std::vector> gamepads; - uinput_t mouse_input; - uinput_t touch_input; - uinput_t keyboard_input; - - evdev_t gamepad_dev; - evdev_t touch_dev; - evdev_t mouse_dev; - evdev_t keyboard_dev; - -#ifdef SUNSHINE_BUILD_X11 - Display *display; -#endif -}; - -inline void rumbleIterate(std::vector &effects, std::vector &polls, std::chrono::milliseconds to) { - std::vector polls_recv; - polls_recv.reserve(polls.size()); - for(auto &poll : polls) { - polls_recv.emplace_back(poll.el); - } - - auto res = poll(polls_recv.data(), polls_recv.size(), to.count()); - - // If timed out - if(!res) { - return; - } - - if(res < 0) { - char err_str[1024]; - BOOST_LOG(error) << "Couldn't poll Gamepad file descriptors: "sv << strerror_r(errno, err_str, 1024); - - return; - } - - for(int x = 0; x < polls.size(); ++x) { - auto poll = std::begin(polls) + x; - auto effect_it = std::begin(effects) + x; - - auto fd = (*poll)->fd; - - // TUPLE_2D_REF(dev, q, *dev_q_it); - - // on error - if(polls_recv[x].revents & (POLLHUP | POLLRDHUP | POLLERR)) { - BOOST_LOG(warning) << "Gamepad ["sv << x << "] file discriptor closed unexpectedly"sv; - - polls.erase(poll); - effects.erase(effect_it); - - --x; - continue; - } - - if(!(polls_recv[x].revents & POLLIN)) { - continue; - } - - input_event events[64]; - - // Read all available events - auto bytes = read(fd, &events, sizeof(events)); - - if(bytes < 0) { - char err_str[1024]; - - BOOST_LOG(error) << "Couldn't read evdev input ["sv << errno << "]: "sv << strerror_r(errno, err_str, 1024); - - polls.erase(poll); - effects.erase(effect_it); - - --x; - continue; - } - - if(bytes < sizeof(input_event)) { - BOOST_LOG(warning) << "Reading evdev input: Expected at least "sv << sizeof(input_event) << " bytes, got "sv << bytes << " instead"sv; - continue; - } - - auto event_count = bytes / sizeof(input_event); - - for(auto event = events; event != (events + event_count); ++event) { - switch(event->type) { - case EV_FF: - // BOOST_LOG(debug) << "EV_FF: "sv << event->value << " aka "sv << util::hex(event->value).to_string_view(); - - if(event->code == FF_GAIN) { - BOOST_LOG(debug) << "EV_FF: code [FF_GAIN]: value: "sv << event->value << " aka "sv << util::hex(event->value).to_string_view(); - effect_it->gain = std::clamp(event->value, 0, 0xFFFF); - - break; + std::pair + rumble(std::chrono::steady_clock::time_point tp) { + if (end_point < tp) { + return {}; } - BOOST_LOG(debug) << "EV_FF: id ["sv << event->code << "]: value: "sv << event->value << " aka "sv << util::hex(event->value).to_string_view(); + auto time_left = + std::chrono::duration_cast( + end_point - tp); - if(event->value) { - effect_it->activate(event->code); + // If it needs to be delayed' + if (time_left > length) { + return {}; } - else { - effect_it->deactivate(event->code); + + auto t = length - time_left; + + auto weak = magnitude(t, start.weak, end.weak); + auto strong = magnitude(t, start.strong, end.strong); + + if (t.count() < envelope.attack_length) { + weak = (envelope.attack_level * t.count() + weak * (envelope.attack_length - t.count())) / envelope.attack_length; + strong = (envelope.attack_level * t.count() + strong * (envelope.attack_length - t.count())) / envelope.attack_length; } - break; - case EV_UINPUT: - switch(event->code) { - case UI_FF_UPLOAD: { - uinput_ff_upload upload {}; + else if (time_left.count() < envelope.fade_length) { + auto dt = (t - length).count() + envelope.fade_length; - // *VERY* important, without this you break - // the kernel and have to reboot due to dead - // hanging process - upload.request_id = event->value; - - ioctl(fd, UI_BEGIN_FF_UPLOAD, &upload); - auto fg = util::fail_guard([&]() { - upload.retval = 0; - ioctl(fd, UI_END_FF_UPLOAD, &upload); - }); - - effect_it->upload(upload.effect); - } break; - case UI_FF_ERASE: { - uinput_ff_erase erase {}; - - // *VERY* important, without this you break - // the kernel and have to reboot due to dead - // hanging process - erase.request_id = event->value; - - ioctl(fd, UI_BEGIN_FF_ERASE, &erase); - auto fg = util::fail_guard([&]() { - erase.retval = 0; - ioctl(fd, UI_END_FF_ERASE, &erase); - }); - - effect_it->erase(erase.effect_id); - } break; + weak = (envelope.fade_level * dt + weak * (envelope.fade_length - dt)) / envelope.fade_length; + strong = (envelope.fade_level * dt + strong * (envelope.fade_length - dt)) / envelope.fade_length; } - break; - default: - BOOST_LOG(debug) - << util::hex(event->type).to_string_view() << ": "sv - << util::hex(event->code).to_string_view() << ": "sv - << event->value << " aka "sv << util::hex(event->value).to_string_view(); + + return { + weak, strong + }; } + + void + activate() { + end_point = std::chrono::steady_clock::now() + delay + length; + } + + void + deactivate() { + end_point = std::chrono::steady_clock::time_point::min(); + } + + std::chrono::milliseconds delay; + std::chrono::milliseconds length; + + std::chrono::steady_clock::time_point end_point; + + ff_envelope envelope; + struct { + std::uint32_t weak, strong; + } start; + + struct { + std::uint32_t weak, strong; + } end; + }; + + std::pair + rumble(std::chrono::steady_clock::time_point tp) { + std::pair weak_strong {}; + for (auto &[_, data] : id_to_data) { + weak_strong += data.rumble(tp); + } + + std::clamp(weak_strong.first, 0, 0xFFFF); + std::clamp(weak_strong.second, 0, 0xFFFF); + + old_rumble = weak_strong * gain / 0xFFFF; + return old_rumble; } - } -} -void broadcastRumble(safe::queue_t &rumble_queue_queue) { - std::vector effects; - std::vector polls; + void + upload(const ff_effect &effect) { + print(effect); - while(rumble_queue_queue.running()) { - while(rumble_queue_queue.peek()) { - auto dev_rumble_queue = rumble_queue_queue.pop(); + auto it = id_to_data.find(effect.id); - if(!dev_rumble_queue) { - // rumble_queue_queue is no longer running + if (it == std::end(id_to_data)) { + id_to_data.emplace(effect.id, effect); return; } - auto gamepadnr = std::get<0>(*dev_rumble_queue); - auto dev = std::get<1>(*dev_rumble_queue); - auto &rumble_queue = std::get<2>(*dev_rumble_queue); - auto &pollfd = std::get<3>(*dev_rumble_queue); + data_t data { effect }; - { - auto effect_it = std::find_if(std::begin(effects), std::end(effects), [dev](auto &curr_effect) { - return dev == curr_effect.dev; - }); + data.end_point = it->second.end_point; + it->second = data; + } - if(effect_it != std::end(effects)) { + void + activate(int id) { + auto it = id_to_data.find(id); - auto poll_it = std::begin(polls) + (effect_it - std::begin(effects)); + if (it != std::end(id_to_data)) { + it->second.activate(); + } + } - polls.erase(poll_it); - effects.erase(effect_it); + void + deactivate(int id) { + auto it = id_to_data.find(id); - BOOST_LOG(debug) << "Removed Gamepad device from notifications"sv; + if (it != std::end(id_to_data)) { + it->second.deactivate(); + } + } - continue; - } + void + erase(int id) { + id_to_data.erase(id); + BOOST_LOG(debug) << "Removed rumble effect id ["sv << id << ']'; + } - // There may be an attepmt to remove, that which not exists - if(!rumble_queue) { - BOOST_LOG(warning) << "Attempting to remove a gamepad device from notifications that isn't already registered"sv; - continue; - } + // Used as ID for rumble notifications + int gamepadnr; + + // Used as ID for adding/removinf devices from evdev notifications + uinput_t::pointer dev; + + rumble_queue_t rumble_queue; + + int gain; + + // No need to send rumble data when old values equals the new values + std::pair old_rumble; + + std::unordered_map id_to_data; + }; + + struct rumble_ctx_t { + std::thread rumble_thread; + + safe::queue_t rumble_queue_queue; + }; + + void + broadcastRumble(safe::queue_t &ctx); + int + startRumble(rumble_ctx_t &ctx) { + ctx.rumble_thread = std::thread { broadcastRumble, std::ref(ctx.rumble_queue_queue) }; + + return 0; + } + + void + stopRumble(rumble_ctx_t &ctx) { + ctx.rumble_queue_queue.stop(); + + BOOST_LOG(debug) << "Waiting for Gamepad notifications to stop..."sv; + ctx.rumble_thread.join(); + BOOST_LOG(debug) << "Gamepad notifications stopped"sv; + } + + static auto notifications = safe::make_shared(startRumble, stopRumble); + + struct input_raw_t { + public: + void + clear_touchscreen() { + std::filesystem::path touch_path { appdata() / "sunshine_touchscreen"sv }; + + if (std::filesystem::is_symlink(touch_path)) { + std::filesystem::remove(touch_path); } - polls.emplace_back(std::move(pollfd)); - effects.emplace_back(gamepadnr, dev, std::move(rumble_queue)); - - BOOST_LOG(debug) << "Added Gamepad device to notifications"sv; + touch_input.reset(); } - if(polls.empty()) { - std::this_thread::sleep_for(250ms); + void + clear_keyboard() { + std::filesystem::path key_path { appdata() / "sunshine_keyboard"sv }; + + if (std::filesystem::is_symlink(key_path)) { + std::filesystem::remove(key_path); + } + + keyboard_input.reset(); } - else { - rumbleIterate(effects, polls, 100ms); - auto now = std::chrono::steady_clock::now(); - for(auto &effect : effects) { - TUPLE_2D(old_weak, old_strong, effect.old_rumble); - TUPLE_2D(weak, strong, effect.rumble(now)); + void + clear_mouse() { + std::filesystem::path mouse_path { appdata() / "sunshine_mouse"sv }; - if(old_weak != weak || old_strong != strong) { - BOOST_LOG(debug) << "Sending haptic feedback: lowfreq [0x"sv << util::hex(weak).to_string_view() << "]: highfreq [0x"sv << util::hex(strong).to_string_view() << ']'; + if (std::filesystem::is_symlink(mouse_path)) { + std::filesystem::remove(mouse_path); + } - effect.rumble_queue->raise(effect.gamepadnr, weak, strong); + mouse_input.reset(); + } + + void + clear_gamepad(int nr) { + auto &[dev, _] = gamepads[nr]; + + if (!dev) { + return; + } + + // Remove this gamepad from notifications + rumble_ctx->rumble_queue_queue.raise(nr, dev.get(), nullptr, pollfd_t {}); + + std::stringstream ss; + + ss << "sunshine_gamepad_"sv << nr; + + auto gamepad_path = platf::appdata() / ss.str(); + if (std::filesystem::is_symlink(gamepad_path)) { + std::filesystem::remove(gamepad_path); + } + + gamepads[nr] = std::make_pair(uinput_t {}, gamepad_state_t {}); + } + + int + create_mouse() { + int err = libevdev_uinput_create_from_device(mouse_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &mouse_input); + + if (err) { + BOOST_LOG(error) << "Could not create Sunshine Mouse: "sv << strerror(-err); + return -1; + } + + std::filesystem::create_symlink(libevdev_uinput_get_devnode(mouse_input.get()), appdata() / "sunshine_mouse"sv); + + return 0; + } + + int + create_touchscreen() { + int err = libevdev_uinput_create_from_device(touch_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &touch_input); + + if (err) { + BOOST_LOG(error) << "Could not create Sunshine Touchscreen: "sv << strerror(-err); + return -1; + } + + std::filesystem::create_symlink(libevdev_uinput_get_devnode(touch_input.get()), appdata() / "sunshine_touchscreen"sv); + + return 0; + } + + int + create_keyboard() { + int err = libevdev_uinput_create_from_device(keyboard_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &keyboard_input); + + if (err) { + BOOST_LOG(error) << "Could not create Sunshine Keyboard: "sv << strerror(-err); + return -1; + } + + std::filesystem::create_symlink(libevdev_uinput_get_devnode(keyboard_input.get()), appdata() / "sunshine_keyboard"sv); + + return 0; + } + + int + alloc_gamepad(int nr, rumble_queue_t &&rumble_queue) { + TUPLE_2D_REF(input, gamepad_state, gamepads[nr]); + + int err = libevdev_uinput_create_from_device(gamepad_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &input); + + gamepad_state = gamepad_state_t {}; + + if (err) { + BOOST_LOG(error) << "Could not create Sunshine Gamepad: "sv << strerror(-err); + return -1; + } + + std::stringstream ss; + ss << "sunshine_gamepad_"sv << nr; + auto gamepad_path = platf::appdata() / ss.str(); + + if (std::filesystem::is_symlink(gamepad_path)) { + std::filesystem::remove(gamepad_path); + } + + auto dev_node = libevdev_uinput_get_devnode(input.get()); + + rumble_ctx->rumble_queue_queue.raise( + nr, + input.get(), + std::move(rumble_queue), + pollfd_t { + dup(libevdev_uinput_get_fd(input.get())), + (std::int16_t) POLLIN, + (std::int16_t) 0, + }); + + std::filesystem::create_symlink(dev_node, gamepad_path); + return 0; + } + + void + clear() { + clear_touchscreen(); + clear_keyboard(); + clear_mouse(); + for (int x = 0; x < gamepads.size(); ++x) { + clear_gamepad(x); + } + +#ifdef SUNSHINE_BUILD_X11 + if (display) { + x11::CloseDisplay(display); + display = nullptr; + } +#endif + } + + ~input_raw_t() { + clear(); + } + + safe::shared_t::ptr_t rumble_ctx; + + std::vector> gamepads; + uinput_t mouse_input; + uinput_t touch_input; + uinput_t keyboard_input; + + evdev_t gamepad_dev; + evdev_t touch_dev; + evdev_t mouse_dev; + evdev_t keyboard_dev; + +#ifdef SUNSHINE_BUILD_X11 + Display *display; +#endif + }; + + inline void + rumbleIterate(std::vector &effects, std::vector &polls, std::chrono::milliseconds to) { + std::vector polls_recv; + polls_recv.reserve(polls.size()); + for (auto &poll : polls) { + polls_recv.emplace_back(poll.el); + } + + auto res = poll(polls_recv.data(), polls_recv.size(), to.count()); + + // If timed out + if (!res) { + return; + } + + if (res < 0) { + char err_str[1024]; + BOOST_LOG(error) << "Couldn't poll Gamepad file descriptors: "sv << strerror_r(errno, err_str, 1024); + + return; + } + + for (int x = 0; x < polls.size(); ++x) { + auto poll = std::begin(polls) + x; + auto effect_it = std::begin(effects) + x; + + auto fd = (*poll)->fd; + + // TUPLE_2D_REF(dev, q, *dev_q_it); + + // on error + if (polls_recv[x].revents & (POLLHUP | POLLRDHUP | POLLERR)) { + BOOST_LOG(warning) << "Gamepad ["sv << x << "] file discriptor closed unexpectedly"sv; + + polls.erase(poll); + effects.erase(effect_it); + + --x; + continue; + } + + if (!(polls_recv[x].revents & POLLIN)) { + continue; + } + + input_event events[64]; + + // Read all available events + auto bytes = read(fd, &events, sizeof(events)); + + if (bytes < 0) { + char err_str[1024]; + + BOOST_LOG(error) << "Couldn't read evdev input ["sv << errno << "]: "sv << strerror_r(errno, err_str, 1024); + + polls.erase(poll); + effects.erase(effect_it); + + --x; + continue; + } + + if (bytes < sizeof(input_event)) { + BOOST_LOG(warning) << "Reading evdev input: Expected at least "sv << sizeof(input_event) << " bytes, got "sv << bytes << " instead"sv; + continue; + } + + auto event_count = bytes / sizeof(input_event); + + for (auto event = events; event != (events + event_count); ++event) { + switch (event->type) { + case EV_FF: + // BOOST_LOG(debug) << "EV_FF: "sv << event->value << " aka "sv << util::hex(event->value).to_string_view(); + + if (event->code == FF_GAIN) { + BOOST_LOG(debug) << "EV_FF: code [FF_GAIN]: value: "sv << event->value << " aka "sv << util::hex(event->value).to_string_view(); + effect_it->gain = std::clamp(event->value, 0, 0xFFFF); + + break; + } + + BOOST_LOG(debug) << "EV_FF: id ["sv << event->code << "]: value: "sv << event->value << " aka "sv << util::hex(event->value).to_string_view(); + + if (event->value) { + effect_it->activate(event->code); + } + else { + effect_it->deactivate(event->code); + } + break; + case EV_UINPUT: + switch (event->code) { + case UI_FF_UPLOAD: { + uinput_ff_upload upload {}; + + // *VERY* important, without this you break + // the kernel and have to reboot due to dead + // hanging process + upload.request_id = event->value; + + ioctl(fd, UI_BEGIN_FF_UPLOAD, &upload); + auto fg = util::fail_guard([&]() { + upload.retval = 0; + ioctl(fd, UI_END_FF_UPLOAD, &upload); + }); + + effect_it->upload(upload.effect); + } break; + case UI_FF_ERASE: { + uinput_ff_erase erase {}; + + // *VERY* important, without this you break + // the kernel and have to reboot due to dead + // hanging process + erase.request_id = event->value; + + ioctl(fd, UI_BEGIN_FF_ERASE, &erase); + auto fg = util::fail_guard([&]() { + erase.retval = 0; + ioctl(fd, UI_END_FF_ERASE, &erase); + }); + + effect_it->erase(erase.effect_id); + } break; + } + break; + default: + BOOST_LOG(debug) + << util::hex(event->type).to_string_view() << ": "sv + << util::hex(event->code).to_string_view() << ": "sv + << event->value << " aka "sv << util::hex(event->value).to_string_view(); } } } } -} -/** + void + broadcastRumble(safe::queue_t &rumble_queue_queue) { + std::vector effects; + std::vector polls; + + while (rumble_queue_queue.running()) { + while (rumble_queue_queue.peek()) { + auto dev_rumble_queue = rumble_queue_queue.pop(); + + if (!dev_rumble_queue) { + // rumble_queue_queue is no longer running + return; + } + + auto gamepadnr = std::get<0>(*dev_rumble_queue); + auto dev = std::get<1>(*dev_rumble_queue); + auto &rumble_queue = std::get<2>(*dev_rumble_queue); + auto &pollfd = std::get<3>(*dev_rumble_queue); + + { + auto effect_it = std::find_if(std::begin(effects), std::end(effects), [dev](auto &curr_effect) { + return dev == curr_effect.dev; + }); + + if (effect_it != std::end(effects)) { + auto poll_it = std::begin(polls) + (effect_it - std::begin(effects)); + + polls.erase(poll_it); + effects.erase(effect_it); + + BOOST_LOG(debug) << "Removed Gamepad device from notifications"sv; + + continue; + } + + // There may be an attepmt to remove, that which not exists + if (!rumble_queue) { + BOOST_LOG(warning) << "Attempting to remove a gamepad device from notifications that isn't already registered"sv; + continue; + } + } + + polls.emplace_back(std::move(pollfd)); + effects.emplace_back(gamepadnr, dev, std::move(rumble_queue)); + + BOOST_LOG(debug) << "Added Gamepad device to notifications"sv; + } + + if (polls.empty()) { + std::this_thread::sleep_for(250ms); + } + else { + rumbleIterate(effects, polls, 100ms); + + auto now = std::chrono::steady_clock::now(); + for (auto &effect : effects) { + TUPLE_2D(old_weak, old_strong, effect.old_rumble); + TUPLE_2D(weak, strong, effect.rumble(now)); + + if (old_weak != weak || old_strong != strong) { + BOOST_LOG(debug) << "Sending haptic feedback: lowfreq [0x"sv << util::hex(weak).to_string_view() << "]: highfreq [0x"sv << util::hex(strong).to_string_view() << ']'; + + effect.rumble_queue->raise(effect.gamepadnr, weak, strong); + } + } + } + } + } + + /** * @brief XTest absolute mouse move. * @param input The input_t instance to use. * @param x Absolute x position. @@ -1027,18 +1057,19 @@ void broadcastRumble(safe::queue_t &rumble_queue_queue) { * x_abs_mouse(input, 0, 0); * ``` */ -static void x_abs_mouse(input_t &input, float x, float y) { + static void + x_abs_mouse(input_t &input, float x, float y) { #ifdef SUNSHINE_BUILD_X11 - Display *xdisplay = ((input_raw_t *)input.get())->display; - if(!xdisplay) { - return; - } - x11::tst::FakeMotionEvent(xdisplay, -1, x, y, CurrentTime); - x11::Flush(xdisplay); + Display *xdisplay = ((input_raw_t *) input.get())->display; + if (!xdisplay) { + return; + } + x11::tst::FakeMotionEvent(xdisplay, -1, x, y, CurrentTime); + x11::Flush(xdisplay); #endif -} + } -/** + /** * @brief Absolute mouse move. * @param input The input_t instance to use. * @param touch_port The touch_port instance to use. @@ -1050,25 +1081,26 @@ static void x_abs_mouse(input_t &input, float x, float y) { * abs_mouse(input, touch_port, 0, 0); * ``` */ -void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { - auto touchscreen = ((input_raw_t *)input.get())->touch_input.get(); - if(!touchscreen) { - x_abs_mouse(input, x, y); - return; + void + abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { + auto touchscreen = ((input_raw_t *) input.get())->touch_input.get(); + if (!touchscreen) { + x_abs_mouse(input, x, y); + return; + } + + auto scaled_x = (int) std::lround((x + touch_port.offset_x) * ((float) target_touch_port.width / (float) touch_port.width)); + auto scaled_y = (int) std::lround((y + touch_port.offset_y) * ((float) target_touch_port.height / (float) touch_port.height)); + + libevdev_uinput_write_event(touchscreen, EV_ABS, ABS_X, scaled_x); + libevdev_uinput_write_event(touchscreen, EV_ABS, ABS_Y, scaled_y); + libevdev_uinput_write_event(touchscreen, EV_KEY, BTN_TOOL_FINGER, 1); + libevdev_uinput_write_event(touchscreen, EV_KEY, BTN_TOOL_FINGER, 0); + + libevdev_uinput_write_event(touchscreen, EV_SYN, SYN_REPORT, 0); } - auto scaled_x = (int)std::lround((x + touch_port.offset_x) * ((float)target_touch_port.width / (float)touch_port.width)); - auto scaled_y = (int)std::lround((y + touch_port.offset_y) * ((float)target_touch_port.height / (float)touch_port.height)); - - libevdev_uinput_write_event(touchscreen, EV_ABS, ABS_X, scaled_x); - libevdev_uinput_write_event(touchscreen, EV_ABS, ABS_Y, scaled_y); - libevdev_uinput_write_event(touchscreen, EV_KEY, BTN_TOOL_FINGER, 1); - libevdev_uinput_write_event(touchscreen, EV_KEY, BTN_TOOL_FINGER, 0); - - libevdev_uinput_write_event(touchscreen, EV_SYN, SYN_REPORT, 0); -} - -/** + /** * @brief XTest relative mouse move. * @param input The input_t instance to use. * @param deltaX Relative x position. @@ -1079,18 +1111,19 @@ void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) * x_move_mouse(input, 10, 10); // Move mouse 10 pixels down and right * ``` */ -static void x_move_mouse(input_t &input, int deltaX, int deltaY) { + static void + x_move_mouse(input_t &input, int deltaX, int deltaY) { #ifdef SUNSHINE_BUILD_X11 - Display *xdisplay = ((input_raw_t *)input.get())->display; - if(!xdisplay) { - return; - } - x11::tst::FakeRelativeMotionEvent(xdisplay, deltaX, deltaY, CurrentTime); - x11::Flush(xdisplay); + Display *xdisplay = ((input_raw_t *) input.get())->display; + if (!xdisplay) { + return; + } + x11::tst::FakeRelativeMotionEvent(xdisplay, deltaX, deltaY, CurrentTime); + x11::Flush(xdisplay); #endif -} + } -/** + /** * @brief Relative mouse move. * @param input The input_t instance to use. * @param deltaX Relative x position. @@ -1101,25 +1134,26 @@ static void x_move_mouse(input_t &input, int deltaX, int deltaY) { * move_mouse(input, 10, 10); // Move mouse 10 pixels down and right * ``` */ -void move_mouse(input_t &input, int deltaX, int deltaY) { - auto mouse = ((input_raw_t *)input.get())->mouse_input.get(); - if(!mouse) { - x_move_mouse(input, deltaX, deltaY); - return; + void + move_mouse(input_t &input, int deltaX, int deltaY) { + auto mouse = ((input_raw_t *) input.get())->mouse_input.get(); + if (!mouse) { + x_move_mouse(input, deltaX, deltaY); + return; + } + + if (deltaX) { + libevdev_uinput_write_event(mouse, EV_REL, REL_X, deltaX); + } + + if (deltaY) { + libevdev_uinput_write_event(mouse, EV_REL, REL_Y, deltaY); + } + + libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); } - if(deltaX) { - libevdev_uinput_write_event(mouse, EV_REL, REL_X, deltaX); - } - - if(deltaY) { - libevdev_uinput_write_event(mouse, EV_REL, REL_Y, deltaY); - } - - libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); -} - -/** + /** * @brief XTest mouse button press/release. * @param input The input_t instance to use. * @param button Which mouse button to emulate. @@ -1130,38 +1164,39 @@ void move_mouse(input_t &input, int deltaX, int deltaY) { * x_button_mouse(input, 1, false); // Press left mouse button * ``` */ -static void x_button_mouse(input_t &input, int button, bool release) { + static void + x_button_mouse(input_t &input, int button, bool release) { #ifdef SUNSHINE_BUILD_X11 - unsigned int x_button = 0; - switch(button) { - case BUTTON_LEFT: - x_button = 1; - break; - case BUTTON_MIDDLE: - x_button = 2; - break; - case BUTTON_RIGHT: - x_button = 3; - break; - default: - x_button = (button - 4) + 8; // Button 4 (Moonlight) starts at index 8 (X11) - break; - } + unsigned int x_button = 0; + switch (button) { + case BUTTON_LEFT: + x_button = 1; + break; + case BUTTON_MIDDLE: + x_button = 2; + break; + case BUTTON_RIGHT: + x_button = 3; + break; + default: + x_button = (button - 4) + 8; // Button 4 (Moonlight) starts at index 8 (X11) + break; + } - if(x_button < 1 || x_button > 31) { - return; - } + if (x_button < 1 || x_button > 31) { + return; + } - Display *xdisplay = ((input_raw_t *)input.get())->display; - if(!xdisplay) { - return; - } - x11::tst::FakeButtonEvent(xdisplay, x_button, !release, CurrentTime); - x11::Flush(xdisplay); + Display *xdisplay = ((input_raw_t *) input.get())->display; + if (!xdisplay) { + return; + } + x11::tst::FakeButtonEvent(xdisplay, x_button, !release, CurrentTime); + x11::Flush(xdisplay); #endif -} + } -/** + /** * @brief Mouse button press/release. * @param input The input_t instance to use. * @param button Which mouse button to emulate. @@ -1172,43 +1207,44 @@ static void x_button_mouse(input_t &input, int button, bool release) { * button_mouse(input, 1, false); // Press left mouse button * ``` */ -void button_mouse(input_t &input, int button, bool release) { - auto mouse = ((input_raw_t *)input.get())->mouse_input.get(); - if(!mouse) { - x_button_mouse(input, button, release); - return; + void + button_mouse(input_t &input, int button, bool release) { + auto mouse = ((input_raw_t *) input.get())->mouse_input.get(); + if (!mouse) { + x_button_mouse(input, button, release); + return; + } + + int btn_type; + int scan; + + if (button == 1) { + btn_type = BTN_LEFT; + scan = 90001; + } + else if (button == 2) { + btn_type = BTN_MIDDLE; + scan = 90003; + } + else if (button == 3) { + btn_type = BTN_RIGHT; + scan = 90002; + } + else if (button == 4) { + btn_type = BTN_SIDE; + scan = 90004; + } + else { + btn_type = BTN_EXTRA; + scan = 90005; + } + + libevdev_uinput_write_event(mouse, EV_MSC, MSC_SCAN, scan); + libevdev_uinput_write_event(mouse, EV_KEY, btn_type, release ? 0 : 1); + libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); } - int btn_type; - int scan; - - if(button == 1) { - btn_type = BTN_LEFT; - scan = 90001; - } - else if(button == 2) { - btn_type = BTN_MIDDLE; - scan = 90003; - } - else if(button == 3) { - btn_type = BTN_RIGHT; - scan = 90002; - } - else if(button == 4) { - btn_type = BTN_SIDE; - scan = 90004; - } - else { - btn_type = BTN_EXTRA; - scan = 90005; - } - - libevdev_uinput_write_event(mouse, EV_MSC, MSC_SCAN, scan); - libevdev_uinput_write_event(mouse, EV_KEY, btn_type, release ? 0 : 1); - libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); -} - -/** + /** * @brief XTest mouse scroll. * @param input The input_t instance to use. * @param distance How far to scroll @@ -1220,23 +1256,24 @@ void button_mouse(input_t &input, int button, bool release) { * x_scroll(input, 10, 4, 5); * ``` */ -static void x_scroll(input_t &input, int distance, int button_pos, int button_neg) { + static void + x_scroll(input_t &input, int distance, int button_pos, int button_neg) { #ifdef SUNSHINE_BUILD_X11 - Display *xdisplay = ((input_raw_t *)input.get())->display; - if(!xdisplay) { - return; - } + Display *xdisplay = ((input_raw_t *) input.get())->display; + if (!xdisplay) { + return; + } - const int button = distance > 0 ? button_pos : button_neg; - for(int i = 0; i < abs(distance); i++) { - x11::tst::FakeButtonEvent(xdisplay, button, true, CurrentTime); - x11::tst::FakeButtonEvent(xdisplay, button, false, CurrentTime); - } - x11::Flush(xdisplay); + const int button = distance > 0 ? button_pos : button_neg; + for (int i = 0; i < abs(distance); i++) { + x11::tst::FakeButtonEvent(xdisplay, button, true, CurrentTime); + x11::tst::FakeButtonEvent(xdisplay, button, false, CurrentTime); + } + x11::Flush(xdisplay); #endif -} + } -/** + /** * @brief Vertical mouse scroll. * @param input The input_t instance to use. * @param high_res_distance How far to scroll @@ -1246,21 +1283,22 @@ static void x_scroll(input_t &input, int distance, int button_pos, int button_ne * scroll(input, 1200); * ``` */ -void scroll(input_t &input, int high_res_distance) { - int distance = high_res_distance / 120; + void + scroll(input_t &input, int high_res_distance) { + int distance = high_res_distance / 120; - auto mouse = ((input_raw_t *)input.get())->mouse_input.get(); - if(!mouse) { - x_scroll(input, distance, 4, 5); - return; + auto mouse = ((input_raw_t *) input.get())->mouse_input.get(); + if (!mouse) { + x_scroll(input, distance, 4, 5); + return; + } + + libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL, distance); + libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL_HI_RES, high_res_distance); + libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); } - libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL, distance); - libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL_HI_RES, high_res_distance); - libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); -} - -/** + /** * @brief Horizontal mouse scroll. * @param input The input_t instance to use. * @param high_res_distance How far to scroll @@ -1270,29 +1308,31 @@ void scroll(input_t &input, int high_res_distance) { * hscroll(input, 1200); * ``` */ -void hscroll(input_t &input, int high_res_distance) { - int distance = high_res_distance / 120; + void + hscroll(input_t &input, int high_res_distance) { + int distance = high_res_distance / 120; - auto mouse = ((input_raw_t *)input.get())->mouse_input.get(); - if(!mouse) { - x_scroll(input, distance, 6, 7); - return; + auto mouse = ((input_raw_t *) input.get())->mouse_input.get(); + if (!mouse) { + x_scroll(input, distance, 6, 7); + return; + } + + libevdev_uinput_write_event(mouse, EV_REL, REL_HWHEEL, distance); + libevdev_uinput_write_event(mouse, EV_REL, REL_HWHEEL_HI_RES, high_res_distance); + libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); } - libevdev_uinput_write_event(mouse, EV_REL, REL_HWHEEL, distance); - libevdev_uinput_write_event(mouse, EV_REL, REL_HWHEEL_HI_RES, high_res_distance); - libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); -} + static keycode_t + keysym(std::uint16_t modcode) { + if (modcode <= keycodes.size()) { + return keycodes[modcode]; + } -static keycode_t keysym(std::uint16_t modcode) { - if(modcode <= keycodes.size()) { - return keycodes[modcode]; + return {}; } - return {}; -} - -/** + /** * @brief XTest keyboard emulation. * @param input The input_t instance to use. * @param modcode The moonlight key code. @@ -1303,29 +1343,30 @@ static keycode_t keysym(std::uint16_t modcode) { * x_keyboard(input, 0x5A, false); // Press Z * ``` */ -static void x_keyboard(input_t &input, uint16_t modcode, bool release) { + static void + x_keyboard(input_t &input, uint16_t modcode, bool release) { #ifdef SUNSHINE_BUILD_X11 - auto keycode = keysym(modcode); - if(keycode.keysym == UNKNOWN) { - return; - } + auto keycode = keysym(modcode); + if (keycode.keysym == UNKNOWN) { + return; + } - Display *xdisplay = ((input_raw_t *)input.get())->display; - if(!xdisplay) { - return; - } + Display *xdisplay = ((input_raw_t *) input.get())->display; + if (!xdisplay) { + return; + } - const auto keycode_x = XKeysymToKeycode(xdisplay, keycode.keysym); - if(keycode_x == 0) { - return; - } + const auto keycode_x = XKeysymToKeycode(xdisplay, keycode.keysym); + if (keycode_x == 0) { + return; + } - x11::tst::FakeKeyEvent(xdisplay, keycode_x, !release, CurrentTime); - x11::Flush(xdisplay); + x11::tst::FakeKeyEvent(xdisplay, keycode_x, !release, CurrentTime); + x11::Flush(xdisplay); #endif -} + } -/** + /** * @brief Keyboard emulation. * @param input The input_t instance to use. * @param modcode The moonlight key code. @@ -1336,51 +1377,54 @@ static void x_keyboard(input_t &input, uint16_t modcode, bool release) { * keyboard(input, 0x5A, false); // Press Z * ``` */ -void keyboard(input_t &input, uint16_t modcode, bool release) { - auto keyboard = ((input_raw_t *)input.get())->keyboard_input.get(); - if(!keyboard) { - x_keyboard(input, modcode, release); - return; + void + keyboard(input_t &input, uint16_t modcode, bool release) { + auto keyboard = ((input_raw_t *) input.get())->keyboard_input.get(); + if (!keyboard) { + x_keyboard(input, modcode, release); + return; + } + + auto keycode = keysym(modcode); + if (keycode.keycode == UNKNOWN) { + return; + } + + if (keycode.scancode != UNKNOWN) { + libevdev_uinput_write_event(keyboard, EV_MSC, MSC_SCAN, keycode.scancode); + } + + libevdev_uinput_write_event(keyboard, EV_KEY, keycode.keycode, release ? 0 : 1); + libevdev_uinput_write_event(keyboard, EV_SYN, SYN_REPORT, 0); } - auto keycode = keysym(modcode); - if(keycode.keycode == UNKNOWN) { - return; + void + keyboard_ev(libevdev_uinput *keyboard, int linux_code, int event_code = 1) { + libevdev_uinput_write_event(keyboard, EV_KEY, linux_code, event_code); + libevdev_uinput_write_event(keyboard, EV_SYN, SYN_REPORT, 0); } - if(keycode.scancode != UNKNOWN) { - libevdev_uinput_write_event(keyboard, EV_MSC, MSC_SCAN, keycode.scancode); - } - - libevdev_uinput_write_event(keyboard, EV_KEY, keycode.keycode, release ? 0 : 1); - libevdev_uinput_write_event(keyboard, EV_SYN, SYN_REPORT, 0); -} - -void keyboard_ev(libevdev_uinput *keyboard, int linux_code, int event_code = 1) { - libevdev_uinput_write_event(keyboard, EV_KEY, linux_code, event_code); - libevdev_uinput_write_event(keyboard, EV_SYN, SYN_REPORT, 0); -} - -/** + /** * Takes an UTF-32 encoded string and returns a hex string representation of the bytes (uppercase) * * ex: ['👱'] = "1F471" // see UTF encoding at https://www.compart.com/en/unicode/U+1F471 * * adapted from: https://stackoverflow.com/a/7639754 */ -std::string to_hex(const std::basic_string &str) { - std::stringstream ss; - ss << std::hex << std::setfill('0'); - for(const auto &ch : str) { - ss << ch; + std::string + to_hex(const std::basic_string &str) { + std::stringstream ss; + ss << std::hex << std::setfill('0'); + for (const auto &ch : str) { + ss << ch; + } + + std::string hex_unicode(ss.str()); + std::transform(hex_unicode.begin(), hex_unicode.end(), hex_unicode.begin(), ::toupper); + return hex_unicode; } - std::string hex_unicode(ss.str()); - std::transform(hex_unicode.begin(), hex_unicode.end(), hex_unicode.begin(), ::toupper); - return hex_unicode; -} - -/** + /** * Here we receive a single UTF-8 encoded char at a time, * the trick is to convert it to UTF-32 then send CTRL+SHIFT+U+ in order to produce any * unicode character, see: https://en.wikipedia.org/wiki/Unicode_input @@ -1391,115 +1435,118 @@ std::string to_hex(const std::basic_string &str) { * - then type: CTRL+SHIFT+U+1F471 * see the conversion at: https://www.compart.com/en/unicode/U+1F471 */ -void unicode(input_t &input, char *utf8, int size) { - auto kb = ((input_raw_t *)input.get())->keyboard_input.get(); - if(!kb) { - return; - } - - /* Reading input text as UTF-8 */ - auto utf8_str = boost::locale::conv::to_utf(utf8, utf8 + size, "UTF-8"); - /* Converting to UTF-32 */ - auto utf32_str = boost::locale::conv::utf_to_utf(utf8_str); - /* To HEX string */ - auto hex_unicode = to_hex(utf32_str); - BOOST_LOG(debug) << "Unicode, typing U+"sv << hex_unicode; - - /* pressing + + U */ - keyboard_ev(kb, KEY_LEFTCTRL, 1); - keyboard_ev(kb, KEY_LEFTSHIFT, 1); - keyboard_ev(kb, KEY_U, 1); - keyboard_ev(kb, KEY_U, 0); - - /* input each HEX character */ - for(auto &ch : hex_unicode) { - auto key_str = "KEY_"s + ch; - auto keycode = libevdev_event_code_from_name(EV_KEY, key_str.c_str()); - if(keycode == -1) { - BOOST_LOG(warning) << "Unicode, unable to find keycode for: "sv << ch; - } - else { - keyboard_ev(kb, keycode, 1); - keyboard_ev(kb, keycode, 0); - } - } - - /* releasing and */ - keyboard_ev(kb, KEY_LEFTSHIFT, 0); - keyboard_ev(kb, KEY_LEFTCTRL, 0); -} - -int alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) { - return ((input_raw_t *)input.get())->alloc_gamepad(nr, std::move(rumble_queue)); -} - -void free_gamepad(input_t &input, int nr) { - ((input_raw_t *)input.get())->clear_gamepad(nr); -} - -void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) { - TUPLE_2D_REF(uinput, gamepad_state_old, ((input_raw_t *)input.get())->gamepads[nr]); - - - auto bf = gamepad_state.buttonFlags ^ gamepad_state_old.buttonFlags; - auto bf_new = gamepad_state.buttonFlags; - - if(bf) { - // up pressed == -1, down pressed == 1, else 0 - if((DPAD_UP | DPAD_DOWN) & bf) { - int button_state = bf_new & DPAD_UP ? -1 : (bf_new & DPAD_DOWN ? 1 : 0); - - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_HAT0Y, button_state); + void + unicode(input_t &input, char *utf8, int size) { + auto kb = ((input_raw_t *) input.get())->keyboard_input.get(); + if (!kb) { + return; } - if((DPAD_LEFT | DPAD_RIGHT) & bf) { - int button_state = bf_new & DPAD_LEFT ? -1 : (bf_new & DPAD_RIGHT ? 1 : 0); + /* Reading input text as UTF-8 */ + auto utf8_str = boost::locale::conv::to_utf(utf8, utf8 + size, "UTF-8"); + /* Converting to UTF-32 */ + auto utf32_str = boost::locale::conv::utf_to_utf(utf8_str); + /* To HEX string */ + auto hex_unicode = to_hex(utf32_str); + BOOST_LOG(debug) << "Unicode, typing U+"sv << hex_unicode; - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_HAT0X, button_state); + /* pressing + + U */ + keyboard_ev(kb, KEY_LEFTCTRL, 1); + keyboard_ev(kb, KEY_LEFTSHIFT, 1); + keyboard_ev(kb, KEY_U, 1); + keyboard_ev(kb, KEY_U, 0); + + /* input each HEX character */ + for (auto &ch : hex_unicode) { + auto key_str = "KEY_"s + ch; + auto keycode = libevdev_event_code_from_name(EV_KEY, key_str.c_str()); + if (keycode == -1) { + BOOST_LOG(warning) << "Unicode, unable to find keycode for: "sv << ch; + } + else { + keyboard_ev(kb, keycode, 1); + keyboard_ev(kb, keycode, 0); + } } - if(START & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_START, bf_new & START ? 1 : 0); - if(BACK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SELECT, bf_new & BACK ? 1 : 0); - if(LEFT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBL, bf_new & LEFT_STICK ? 1 : 0); - if(RIGHT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBR, bf_new & RIGHT_STICK ? 1 : 0); - if(LEFT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TL, bf_new & LEFT_BUTTON ? 1 : 0); - if(RIGHT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TR, bf_new & RIGHT_BUTTON ? 1 : 0); - if(HOME & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_MODE, bf_new & HOME ? 1 : 0); - if(A & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SOUTH, bf_new & A ? 1 : 0); - if(B & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_EAST, bf_new & B ? 1 : 0); - if(X & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_NORTH, bf_new & X ? 1 : 0); - if(Y & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_WEST, bf_new & Y ? 1 : 0); + /* releasing and */ + keyboard_ev(kb, KEY_LEFTSHIFT, 0); + keyboard_ev(kb, KEY_LEFTCTRL, 0); } - if(gamepad_state_old.lt != gamepad_state.lt) { - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_Z, gamepad_state.lt); + int + alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) { + return ((input_raw_t *) input.get())->alloc_gamepad(nr, std::move(rumble_queue)); } - if(gamepad_state_old.rt != gamepad_state.rt) { - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RZ, gamepad_state.rt); + void + free_gamepad(input_t &input, int nr) { + ((input_raw_t *) input.get())->clear_gamepad(nr); } - if(gamepad_state_old.lsX != gamepad_state.lsX) { - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_X, gamepad_state.lsX); + void + gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) { + TUPLE_2D_REF(uinput, gamepad_state_old, ((input_raw_t *) input.get())->gamepads[nr]); + + auto bf = gamepad_state.buttonFlags ^ gamepad_state_old.buttonFlags; + auto bf_new = gamepad_state.buttonFlags; + + if (bf) { + // up pressed == -1, down pressed == 1, else 0 + if ((DPAD_UP | DPAD_DOWN) & bf) { + int button_state = bf_new & DPAD_UP ? -1 : (bf_new & DPAD_DOWN ? 1 : 0); + + libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_HAT0Y, button_state); + } + + if ((DPAD_LEFT | DPAD_RIGHT) & bf) { + int button_state = bf_new & DPAD_LEFT ? -1 : (bf_new & DPAD_RIGHT ? 1 : 0); + + libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_HAT0X, button_state); + } + + if (START & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_START, bf_new & START ? 1 : 0); + if (BACK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SELECT, bf_new & BACK ? 1 : 0); + if (LEFT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBL, bf_new & LEFT_STICK ? 1 : 0); + if (RIGHT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBR, bf_new & RIGHT_STICK ? 1 : 0); + if (LEFT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TL, bf_new & LEFT_BUTTON ? 1 : 0); + if (RIGHT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TR, bf_new & RIGHT_BUTTON ? 1 : 0); + if (HOME & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_MODE, bf_new & HOME ? 1 : 0); + if (A & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SOUTH, bf_new & A ? 1 : 0); + if (B & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_EAST, bf_new & B ? 1 : 0); + if (X & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_NORTH, bf_new & X ? 1 : 0); + if (Y & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_WEST, bf_new & Y ? 1 : 0); + } + + if (gamepad_state_old.lt != gamepad_state.lt) { + libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_Z, gamepad_state.lt); + } + + if (gamepad_state_old.rt != gamepad_state.rt) { + libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RZ, gamepad_state.rt); + } + + if (gamepad_state_old.lsX != gamepad_state.lsX) { + libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_X, gamepad_state.lsX); + } + + if (gamepad_state_old.lsY != gamepad_state.lsY) { + libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_Y, -gamepad_state.lsY); + } + + if (gamepad_state_old.rsX != gamepad_state.rsX) { + libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RX, gamepad_state.rsX); + } + + if (gamepad_state_old.rsY != gamepad_state.rsY) { + libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RY, -gamepad_state.rsY); + } + + gamepad_state_old = gamepad_state; + libevdev_uinput_write_event(uinput.get(), EV_SYN, SYN_REPORT, 0); } - if(gamepad_state_old.lsY != gamepad_state.lsY) { - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_Y, -gamepad_state.lsY); - } - - if(gamepad_state_old.rsX != gamepad_state.rsX) { - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RX, gamepad_state.rsX); - } - - if(gamepad_state_old.rsY != gamepad_state.rsY) { - libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RY, -gamepad_state.rsY); - } - - gamepad_state_old = gamepad_state; - libevdev_uinput_write_event(uinput.get(), EV_SYN, SYN_REPORT, 0); -} - -/** + /** * @brief Initalize a new keyboard and return it. * * EXAMPLES: @@ -1507,27 +1554,28 @@ void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) { * auto my_keyboard = keyboard(); * ``` */ -evdev_t keyboard() { - evdev_t dev { libevdev_new() }; + evdev_t + keyboard() { + evdev_t dev { libevdev_new() }; - libevdev_set_uniq(dev.get(), "Sunshine Keyboard"); - libevdev_set_id_product(dev.get(), 0xDEAD); - libevdev_set_id_vendor(dev.get(), 0xBEEF); - libevdev_set_id_bustype(dev.get(), 0x3); - libevdev_set_id_version(dev.get(), 0x111); - libevdev_set_name(dev.get(), "Keyboard passthrough"); + libevdev_set_uniq(dev.get(), "Sunshine Keyboard"); + libevdev_set_id_product(dev.get(), 0xDEAD); + libevdev_set_id_vendor(dev.get(), 0xBEEF); + libevdev_set_id_bustype(dev.get(), 0x3); + libevdev_set_id_version(dev.get(), 0x111); + libevdev_set_name(dev.get(), "Keyboard passthrough"); - libevdev_enable_event_type(dev.get(), EV_KEY); - for(const auto &keycode : keycodes) { - libevdev_enable_event_code(dev.get(), EV_KEY, keycode.keycode, nullptr); + libevdev_enable_event_type(dev.get(), EV_KEY); + for (const auto &keycode : keycodes) { + libevdev_enable_event_code(dev.get(), EV_KEY, keycode.keycode, nullptr); + } + libevdev_enable_event_type(dev.get(), EV_MSC); + libevdev_enable_event_code(dev.get(), EV_MSC, MSC_SCAN, nullptr); + + return dev; } - libevdev_enable_event_type(dev.get(), EV_MSC); - libevdev_enable_event_code(dev.get(), EV_MSC, MSC_SCAN, nullptr); - return dev; -} - -/** + /** * @brief Initalize a new uinput virtual mouse and return it. * * EXAMPLES: @@ -1535,49 +1583,50 @@ evdev_t keyboard() { * auto my_mouse = mouse(); * ``` */ -evdev_t mouse() { - evdev_t dev { libevdev_new() }; + evdev_t + mouse() { + evdev_t dev { libevdev_new() }; - libevdev_set_uniq(dev.get(), "Sunshine Mouse"); - libevdev_set_id_product(dev.get(), 0x4038); - libevdev_set_id_vendor(dev.get(), 0x46D); - libevdev_set_id_bustype(dev.get(), 0x3); - libevdev_set_id_version(dev.get(), 0x111); - libevdev_set_name(dev.get(), "Logitech Wireless Mouse PID:4038"); + libevdev_set_uniq(dev.get(), "Sunshine Mouse"); + libevdev_set_id_product(dev.get(), 0x4038); + libevdev_set_id_vendor(dev.get(), 0x46D); + libevdev_set_id_bustype(dev.get(), 0x3); + libevdev_set_id_version(dev.get(), 0x111); + libevdev_set_name(dev.get(), "Logitech Wireless Mouse PID:4038"); - libevdev_enable_event_type(dev.get(), EV_KEY); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_LEFT, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_RIGHT, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_MIDDLE, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SIDE, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_EXTRA, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_FORWARD, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_BACK, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TASK, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 280, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 281, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 282, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 283, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 284, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 285, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 286, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, 287, nullptr); + libevdev_enable_event_type(dev.get(), EV_KEY); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_LEFT, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_RIGHT, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_MIDDLE, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SIDE, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_EXTRA, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_FORWARD, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_BACK, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TASK, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, 280, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, 281, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, 282, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, 283, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, 284, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, 285, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, 286, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, 287, nullptr); - libevdev_enable_event_type(dev.get(), EV_REL); - libevdev_enable_event_code(dev.get(), EV_REL, REL_X, nullptr); - libevdev_enable_event_code(dev.get(), EV_REL, REL_Y, nullptr); - libevdev_enable_event_code(dev.get(), EV_REL, REL_WHEEL, nullptr); - libevdev_enable_event_code(dev.get(), EV_REL, REL_WHEEL_HI_RES, nullptr); - libevdev_enable_event_code(dev.get(), EV_REL, REL_HWHEEL, nullptr); - libevdev_enable_event_code(dev.get(), EV_REL, REL_HWHEEL_HI_RES, nullptr); + libevdev_enable_event_type(dev.get(), EV_REL); + libevdev_enable_event_code(dev.get(), EV_REL, REL_X, nullptr); + libevdev_enable_event_code(dev.get(), EV_REL, REL_Y, nullptr); + libevdev_enable_event_code(dev.get(), EV_REL, REL_WHEEL, nullptr); + libevdev_enable_event_code(dev.get(), EV_REL, REL_WHEEL_HI_RES, nullptr); + libevdev_enable_event_code(dev.get(), EV_REL, REL_HWHEEL, nullptr); + libevdev_enable_event_code(dev.get(), EV_REL, REL_HWHEEL_HI_RES, nullptr); - libevdev_enable_event_type(dev.get(), EV_MSC); - libevdev_enable_event_code(dev.get(), EV_MSC, MSC_SCAN, nullptr); + libevdev_enable_event_type(dev.get(), EV_MSC); + libevdev_enable_event_code(dev.get(), EV_MSC, MSC_SCAN, nullptr); - return dev; -} + return dev; + } -/** + /** * @brief Initalize a new uinput virtual touchscreen and return it. * * EXAMPLES: @@ -1585,49 +1634,49 @@ evdev_t mouse() { * auto my_touchscreen = touchscreen(); * ``` */ -evdev_t touchscreen() { - evdev_t dev { libevdev_new() }; + evdev_t + touchscreen() { + evdev_t dev { libevdev_new() }; - libevdev_set_uniq(dev.get(), "Sunshine Touch"); - libevdev_set_id_product(dev.get(), 0xDEAD); - libevdev_set_id_vendor(dev.get(), 0xBEEF); - libevdev_set_id_bustype(dev.get(), 0x3); - libevdev_set_id_version(dev.get(), 0x111); - libevdev_set_name(dev.get(), "Touchscreen passthrough"); + libevdev_set_uniq(dev.get(), "Sunshine Touch"); + libevdev_set_id_product(dev.get(), 0xDEAD); + libevdev_set_id_vendor(dev.get(), 0xBEEF); + libevdev_set_id_bustype(dev.get(), 0x3); + libevdev_set_id_version(dev.get(), 0x111); + libevdev_set_name(dev.get(), "Touchscreen passthrough"); - libevdev_enable_property(dev.get(), INPUT_PROP_DIRECT); + libevdev_enable_property(dev.get(), INPUT_PROP_DIRECT); + libevdev_enable_event_type(dev.get(), EV_KEY); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOUCH, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_PEN, nullptr); // Needed to be enabled for BTN_TOOL_FINGER to work. + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_FINGER, nullptr); - libevdev_enable_event_type(dev.get(), EV_KEY); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOUCH, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_PEN, nullptr); // Needed to be enabled for BTN_TOOL_FINGER to work. - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_FINGER, nullptr); + input_absinfo absx { + 0, + 0, + target_touch_port.width, + 1, + 0, + 28 + }; - input_absinfo absx { - 0, - 0, - target_touch_port.width, - 1, - 0, - 28 - }; + input_absinfo absy { + 0, + 0, + target_touch_port.height, + 1, + 0, + 28 + }; + libevdev_enable_event_type(dev.get(), EV_ABS); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &absx); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &absy); - input_absinfo absy { - 0, - 0, - target_touch_port.height, - 1, - 0, - 28 - }; - libevdev_enable_event_type(dev.get(), EV_ABS); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &absx); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &absy); + return dev; + } - return dev; -} - -/** + /** * @brief Initalize a new uinput virtual X360 gamepad and return it. * * EXAMPLES: @@ -1635,75 +1684,76 @@ evdev_t touchscreen() { * auto my_x360 = x360(); * ``` */ -evdev_t x360() { - evdev_t dev { libevdev_new() }; + evdev_t + x360() { + evdev_t dev { libevdev_new() }; - input_absinfo stick { - 0, - -32768, 32767, - 16, - 128, - 0 - }; + input_absinfo stick { + 0, + -32768, 32767, + 16, + 128, + 0 + }; - input_absinfo trigger { - 0, - 0, 255, - 0, - 0, - 0 - }; + input_absinfo trigger { + 0, + 0, 255, + 0, + 0, + 0 + }; - input_absinfo dpad { - 0, - -1, 1, - 0, - 0, - 0 - }; + input_absinfo dpad { + 0, + -1, 1, + 0, + 0, + 0 + }; - libevdev_set_uniq(dev.get(), "Sunshine Gamepad"); - libevdev_set_id_product(dev.get(), 0x28E); - libevdev_set_id_vendor(dev.get(), 0x45E); - libevdev_set_id_bustype(dev.get(), 0x3); - libevdev_set_id_version(dev.get(), 0x110); - libevdev_set_name(dev.get(), "Microsoft X-Box 360 pad"); + libevdev_set_uniq(dev.get(), "Sunshine Gamepad"); + libevdev_set_id_product(dev.get(), 0x28E); + libevdev_set_id_vendor(dev.get(), 0x45E); + libevdev_set_id_bustype(dev.get(), 0x3); + libevdev_set_id_version(dev.get(), 0x110); + libevdev_set_name(dev.get(), "Microsoft X-Box 360 pad"); - libevdev_enable_event_type(dev.get(), EV_KEY); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_WEST, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_EAST, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_NORTH, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SOUTH, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_THUMBL, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_THUMBR, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TR, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TL, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SELECT, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_MODE, nullptr); - libevdev_enable_event_code(dev.get(), EV_KEY, BTN_START, nullptr); + libevdev_enable_event_type(dev.get(), EV_KEY); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_WEST, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_EAST, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_NORTH, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SOUTH, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_THUMBL, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_THUMBR, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TR, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TL, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SELECT, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_MODE, nullptr); + libevdev_enable_event_code(dev.get(), EV_KEY, BTN_START, nullptr); - libevdev_enable_event_type(dev.get(), EV_ABS); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_HAT0Y, &dpad); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_HAT0X, &dpad); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Z, &trigger); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RZ, &trigger); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &stick); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RX, &stick); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &stick); - libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RY, &stick); + libevdev_enable_event_type(dev.get(), EV_ABS); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_HAT0Y, &dpad); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_HAT0X, &dpad); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Z, &trigger); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RZ, &trigger); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &stick); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RX, &stick); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &stick); + libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RY, &stick); - libevdev_enable_event_type(dev.get(), EV_FF); - libevdev_enable_event_code(dev.get(), EV_FF, FF_RUMBLE, nullptr); - libevdev_enable_event_code(dev.get(), EV_FF, FF_CONSTANT, nullptr); - libevdev_enable_event_code(dev.get(), EV_FF, FF_PERIODIC, nullptr); - libevdev_enable_event_code(dev.get(), EV_FF, FF_SINE, nullptr); - libevdev_enable_event_code(dev.get(), EV_FF, FF_RAMP, nullptr); - libevdev_enable_event_code(dev.get(), EV_FF, FF_GAIN, nullptr); + libevdev_enable_event_type(dev.get(), EV_FF); + libevdev_enable_event_code(dev.get(), EV_FF, FF_RUMBLE, nullptr); + libevdev_enable_event_code(dev.get(), EV_FF, FF_CONSTANT, nullptr); + libevdev_enable_event_code(dev.get(), EV_FF, FF_PERIODIC, nullptr); + libevdev_enable_event_code(dev.get(), EV_FF, FF_SINE, nullptr); + libevdev_enable_event_code(dev.get(), EV_FF, FF_RAMP, nullptr); + libevdev_enable_event_code(dev.get(), EV_FF, FF_GAIN, nullptr); - return dev; -} + return dev; + } -/** + /** * @brief Initalize the input system and return it. * * EXAMPLES: @@ -1711,52 +1761,55 @@ evdev_t x360() { * auto my_input = input(); * ``` */ -input_t input() { - input_t result { new input_raw_t() }; - auto &gp = *(input_raw_t *)result.get(); + input_t + input() { + input_t result { new input_raw_t() }; + auto &gp = *(input_raw_t *) result.get(); - gp.rumble_ctx = notifications.ref(); + gp.rumble_ctx = notifications.ref(); - gp.gamepads.resize(MAX_GAMEPADS); + gp.gamepads.resize(MAX_GAMEPADS); - // Ensure starting from clean slate - gp.clear(); - gp.keyboard_dev = keyboard(); - gp.touch_dev = touchscreen(); - gp.mouse_dev = mouse(); - gp.gamepad_dev = x360(); + // Ensure starting from clean slate + gp.clear(); + gp.keyboard_dev = keyboard(); + gp.touch_dev = touchscreen(); + gp.mouse_dev = mouse(); + gp.gamepad_dev = x360(); - gp.create_mouse(); - gp.create_touchscreen(); - gp.create_keyboard(); + gp.create_mouse(); + gp.create_touchscreen(); + gp.create_keyboard(); - // If we do not have a keyboard, touchscreen, or mouse, fall back to XTest - if(!gp.mouse_input || !gp.touch_input || !gp.keyboard_input) { - BOOST_LOG(error) << "Unable to create some input devices! Are you a member of the 'input' group?"sv; + // If we do not have a keyboard, touchscreen, or mouse, fall back to XTest + if (!gp.mouse_input || !gp.touch_input || !gp.keyboard_input) { + BOOST_LOG(error) << "Unable to create some input devices! Are you a member of the 'input' group?"sv; #ifdef SUNSHINE_BUILD_X11 - if(x11::init() || x11::tst::init()) { - BOOST_LOG(error) << "Unable to initialize X11 and/or XTest fallback"sv; - } - else { - BOOST_LOG(info) << "Falling back to XTest"sv; - x11::InitThreads(); - gp.display = x11::OpenDisplay(NULL); - } + if (x11::init() || x11::tst::init()) { + BOOST_LOG(error) << "Unable to initialize X11 and/or XTest fallback"sv; + } + else { + BOOST_LOG(info) << "Falling back to XTest"sv; + x11::InitThreads(); + gp.display = x11::OpenDisplay(NULL); + } #endif + } + + return result; } - return result; -} + void + freeInput(void *p) { + auto *input = (input_raw_t *) p; + delete input; + } -void freeInput(void *p) { - auto *input = (input_raw_t *)p; - delete input; -} + std::vector & + supported_gamepads() { + static std::vector gamepads { "x360"sv }; -std::vector &supported_gamepads() { - static std::vector gamepads { "x360"sv }; - - return gamepads; -} -} // namespace platf + return gamepads; + } +} // namespace platf diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index d8b4a463..392bb65d 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -25,911 +25,952 @@ namespace fs = std::filesystem; namespace platf { -namespace kms { + namespace kms { -class cap_sys_admin { -public: - cap_sys_admin() { - caps = cap_get_proc(); + class cap_sys_admin { + public: + cap_sys_admin() { + caps = cap_get_proc(); - cap_value_t sys_admin = CAP_SYS_ADMIN; - if(cap_set_flag(caps, CAP_EFFECTIVE, 1, &sys_admin, CAP_SET) || cap_set_proc(caps)) { - BOOST_LOG(error) << "Failed to gain CAP_SYS_ADMIN"; + cap_value_t sys_admin = CAP_SYS_ADMIN; + if (cap_set_flag(caps, CAP_EFFECTIVE, 1, &sys_admin, CAP_SET) || cap_set_proc(caps)) { + BOOST_LOG(error) << "Failed to gain CAP_SYS_ADMIN"; + } + } + + ~cap_sys_admin() { + cap_value_t sys_admin = CAP_SYS_ADMIN; + if (cap_set_flag(caps, CAP_EFFECTIVE, 1, &sys_admin, CAP_CLEAR) || cap_set_proc(caps)) { + BOOST_LOG(error) << "Failed to drop CAP_SYS_ADMIN"; + } + cap_free(caps); + } + + cap_t caps; + }; + + class wrapper_fb { + public: + wrapper_fb(drmModeFB *fb): + fb { fb }, fb_id { fb->fb_id }, width { fb->width }, height { fb->height } { + pixel_format = DRM_FORMAT_XRGB8888; + modifier = DRM_FORMAT_MOD_INVALID; + std::fill_n(handles, 4, 0); + std::fill_n(pitches, 4, 0); + std::fill_n(offsets, 4, 0); + handles[0] = fb->handle; + pitches[0] = fb->pitch; + } + + wrapper_fb(drmModeFB2 *fb2): + fb2 { fb2 }, fb_id { fb2->fb_id }, width { fb2->width }, height { fb2->height } { + pixel_format = fb2->pixel_format; + modifier = (fb2->flags & DRM_MODE_FB_MODIFIERS) ? fb2->modifier : DRM_FORMAT_MOD_INVALID; + + memcpy(handles, fb2->handles, sizeof(handles)); + memcpy(pitches, fb2->pitches, sizeof(pitches)); + memcpy(offsets, fb2->offsets, sizeof(offsets)); + } + + ~wrapper_fb() { + if (fb) { + drmModeFreeFB(fb); + } + else if (fb2) { + drmModeFreeFB2(fb2); + } + } + + drmModeFB *fb = nullptr; + drmModeFB2 *fb2 = nullptr; + uint32_t fb_id; + uint32_t width; + uint32_t height; + uint32_t pixel_format; + uint64_t modifier; + uint32_t handles[4]; + uint32_t pitches[4]; + uint32_t offsets[4]; + }; + + using plane_res_t = util::safe_ptr; + using encoder_t = util::safe_ptr; + using res_t = util::safe_ptr; + using plane_t = util::safe_ptr; + using fb_t = std::unique_ptr; + using crtc_t = util::safe_ptr; + using obj_prop_t = util::safe_ptr; + using prop_t = util::safe_ptr; + + using conn_type_count_t = std::map; + + static int env_width; + static int env_height; + + std::string_view + plane_type(std::uint64_t val) { + switch (val) { + case DRM_PLANE_TYPE_OVERLAY: + return "DRM_PLANE_TYPE_OVERLAY"sv; + case DRM_PLANE_TYPE_PRIMARY: + return "DRM_PLANE_TYPE_PRIMARY"sv; + case DRM_PLANE_TYPE_CURSOR: + return "DRM_PLANE_TYPE_CURSOR"sv; + } + + return "UNKNOWN"sv; } - } - ~cap_sys_admin() { - cap_value_t sys_admin = CAP_SYS_ADMIN; - if(cap_set_flag(caps, CAP_EFFECTIVE, 1, &sys_admin, CAP_CLEAR) || cap_set_proc(caps)) { - BOOST_LOG(error) << "Failed to drop CAP_SYS_ADMIN"; - } - cap_free(caps); - } + struct connector_t { + // For example: HDMI-A or HDMI + std::uint32_t type; - cap_t caps; -}; + // Equals zero if not applicable + std::uint32_t crtc_id; -class wrapper_fb { -public: - wrapper_fb(drmModeFB *fb) - : fb { fb }, fb_id { fb->fb_id }, width { fb->width }, height { fb->height } { - pixel_format = DRM_FORMAT_XRGB8888; - modifier = DRM_FORMAT_MOD_INVALID; - std::fill_n(handles, 4, 0); - std::fill_n(pitches, 4, 0); - std::fill_n(offsets, 4, 0); - handles[0] = fb->handle; - pitches[0] = fb->pitch; - } + // For example HDMI-A-{index} or HDMI-{index} + std::uint32_t index; - wrapper_fb(drmModeFB2 *fb2) - : fb2 { fb2 }, fb_id { fb2->fb_id }, width { fb2->width }, height { fb2->height } { - pixel_format = fb2->pixel_format; - modifier = (fb2->flags & DRM_MODE_FB_MODIFIERS) ? fb2->modifier : DRM_FORMAT_MOD_INVALID; + bool connected; + }; - memcpy(handles, fb2->handles, sizeof(handles)); - memcpy(pitches, fb2->pitches, sizeof(pitches)); - memcpy(offsets, fb2->offsets, sizeof(offsets)); - } + struct monitor_t { + std::uint32_t type; - ~wrapper_fb() { - if(fb) { - drmModeFreeFB(fb); - } - else if(fb2) { - drmModeFreeFB2(fb2); - } - } + std::uint32_t index; - drmModeFB *fb = nullptr; - drmModeFB2 *fb2 = nullptr; - uint32_t fb_id; - uint32_t width; - uint32_t height; - uint32_t pixel_format; - uint64_t modifier; - uint32_t handles[4]; - uint32_t pitches[4]; - uint32_t offsets[4]; -}; + platf::touch_port_t viewport; + }; -using plane_res_t = util::safe_ptr; -using encoder_t = util::safe_ptr; -using res_t = util::safe_ptr; -using plane_t = util::safe_ptr; -using fb_t = std::unique_ptr; -using crtc_t = util::safe_ptr; -using obj_prop_t = util::safe_ptr; -using prop_t = util::safe_ptr; + struct card_descriptor_t { + std::string path; -using conn_type_count_t = std::map; + std::map crtc_to_monitor; + }; -static int env_width; -static int env_height; + static std::vector card_descriptors; -std::string_view plane_type(std::uint64_t val) { - switch(val) { - case DRM_PLANE_TYPE_OVERLAY: - return "DRM_PLANE_TYPE_OVERLAY"sv; - case DRM_PLANE_TYPE_PRIMARY: - return "DRM_PLANE_TYPE_PRIMARY"sv; - case DRM_PLANE_TYPE_CURSOR: - return "DRM_PLANE_TYPE_CURSOR"sv; - } - - return "UNKNOWN"sv; -} - -struct connector_t { - // For example: HDMI-A or HDMI - std::uint32_t type; - - // Equals zero if not applicable - std::uint32_t crtc_id; - - // For example HDMI-A-{index} or HDMI-{index} - std::uint32_t index; - - bool connected; -}; - -struct monitor_t { - std::uint32_t type; - - std::uint32_t index; - - platf::touch_port_t viewport; -}; - -struct card_descriptor_t { - std::string path; - - std::map crtc_to_monitor; -}; - -static std::vector card_descriptors; - -static std::uint32_t from_view(const std::string_view &string) { + static std::uint32_t + from_view(const std::string_view &string) { #define _CONVERT(x, y) \ - if(string == x) return DRM_MODE_CONNECTOR_##y + if (string == x) return DRM_MODE_CONNECTOR_##y - _CONVERT("VGA"sv, VGA); - _CONVERT("DVI-I"sv, DVII); - _CONVERT("DVI-D"sv, DVID); - _CONVERT("DVI-A"sv, DVIA); - _CONVERT("S-Video"sv, SVIDEO); - _CONVERT("LVDS"sv, LVDS); - _CONVERT("DIN"sv, 9PinDIN); - _CONVERT("DisplayPort"sv, DisplayPort); - _CONVERT("DP"sv, DisplayPort); - _CONVERT("HDMI-A"sv, HDMIA); - _CONVERT("HDMI"sv, HDMIA); - _CONVERT("HDMI-B"sv, HDMIB); - _CONVERT("eDP"sv, eDP); - _CONVERT("DSI"sv, DSI); + _CONVERT("VGA"sv, VGA); + _CONVERT("DVI-I"sv, DVII); + _CONVERT("DVI-D"sv, DVID); + _CONVERT("DVI-A"sv, DVIA); + _CONVERT("S-Video"sv, SVIDEO); + _CONVERT("LVDS"sv, LVDS); + _CONVERT("DIN"sv, 9PinDIN); + _CONVERT("DisplayPort"sv, DisplayPort); + _CONVERT("DP"sv, DisplayPort); + _CONVERT("HDMI-A"sv, HDMIA); + _CONVERT("HDMI"sv, HDMIA); + _CONVERT("HDMI-B"sv, HDMIB); + _CONVERT("eDP"sv, eDP); + _CONVERT("DSI"sv, DSI); - BOOST_LOG(error) << "Unknown Monitor connector type ["sv << string << "]: Please report this to the GitHub issue tracker"sv; - return DRM_MODE_CONNECTOR_Unknown; -} + BOOST_LOG(error) << "Unknown Monitor connector type ["sv << string << "]: Please report this to the GitHub issue tracker"sv; + return DRM_MODE_CONNECTOR_Unknown; + } -class plane_it_t : public round_robin_util::it_wrap_t { -public: - plane_it_t(int fd, std::uint32_t *plane_p, std::uint32_t *end) - : fd { fd }, plane_p { plane_p }, end { end } { - inc(); - } - - plane_it_t(int fd, std::uint32_t *end) - : fd { fd }, plane_p { end }, end { end } {} - - void inc() { - this->plane.reset(); - - for(; plane_p != end; ++plane_p) { - plane_t plane = drmModeGetPlane(fd, *plane_p); - - if(!plane) { - BOOST_LOG(error) << "Couldn't get drm plane ["sv << (end - plane_p) << "]: "sv << strerror(errno); - continue; + class plane_it_t: public round_robin_util::it_wrap_t { + public: + plane_it_t(int fd, std::uint32_t *plane_p, std::uint32_t *end): + fd { fd }, plane_p { plane_p }, end { end } { + inc(); } - // If this plane is unused - if(plane->fb_id) { - this->plane = util::make_shared(plane.release()); + plane_it_t(int fd, std::uint32_t *end): + fd { fd }, plane_p { end }, end { end } {} - // One last increment - ++plane_p; - break; - } - } - } + void + inc() { + this->plane.reset(); - bool eq(const plane_it_t &other) const { - return plane_p == other.plane_p; - } + for (; plane_p != end; ++plane_p) { + plane_t plane = drmModeGetPlane(fd, *plane_p); - plane_t::pointer get() { - return plane.get(); - } - - int fd; - std::uint32_t *plane_p; - std::uint32_t *end; - - util::shared_t plane; -}; - -class card_t { -public: - using connector_interal_t = util::safe_ptr; - - int init(const char *path) { - cap_sys_admin admin; - fd.el = open(path, O_RDWR); - - if(fd.el < 0) { - BOOST_LOG(error) << "Couldn't open: "sv << path << ": "sv << strerror(errno); - return -1; - } - - if(drmSetClientCap(fd.el, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)) { - BOOST_LOG(error) << "Couldn't expose some/all drm planes for card: "sv << path; - return -1; - } - - if(drmSetClientCap(fd.el, DRM_CLIENT_CAP_ATOMIC, 1)) { - BOOST_LOG(warning) << "Couldn't expose some properties for card: "sv << path; - } - - plane_res.reset(drmModeGetPlaneResources(fd.el)); - if(!plane_res) { - BOOST_LOG(error) << "Couldn't get drm plane resources"sv; - return -1; - } - - return 0; - } - - fb_t fb(plane_t::pointer plane) { - cap_sys_admin admin; - - auto fb2 = drmModeGetFB2(fd.el, plane->fb_id); - if(fb2) { - return std::make_unique(fb2); - } - - auto fb = drmModeGetFB(fd.el, plane->fb_id); - if(fb) { - return std::make_unique(fb); - } - - return nullptr; - } - - crtc_t crtc(std::uint32_t id) { - return drmModeGetCrtc(fd.el, id); - } - - encoder_t encoder(std::uint32_t id) { - return drmModeGetEncoder(fd.el, id); - } - - res_t res() { - return drmModeGetResources(fd.el); - } - - bool is_cursor(std::uint32_t plane_id) { - auto props = plane_props(plane_id); - for(auto &[prop, val] : props) { - if(prop->name == "type"sv) { - if(val == DRM_PLANE_TYPE_CURSOR) { - return true; - } - else { - return false; - } - } - } - - return false; - } - - std::uint32_t get_panel_orientation(std::uint32_t plane_id) { - auto props = plane_props(plane_id); - for(auto &[prop, val] : props) { - if(prop->name == "rotation"sv) { - return val; - } - } - - BOOST_LOG(error) << "Failed to determine panel orientation, defaulting to landscape."; - return DRM_MODE_ROTATE_0; - } - - connector_interal_t connector(std::uint32_t id) { - return drmModeGetConnector(fd.el, id); - } - - std::vector monitors(conn_type_count_t &conn_type_count) { - auto resources = res(); - if(!resources) { - BOOST_LOG(error) << "Couldn't get connector resources"sv; - return {}; - } - - std::vector monitors; - std::for_each_n(resources->connectors, resources->count_connectors, [this, &conn_type_count, &monitors](std::uint32_t id) { - auto conn = connector(id); - - std::uint32_t crtc_id = 0; - - if(conn->encoder_id) { - auto enc = encoder(conn->encoder_id); - if(enc) { - crtc_id = enc->crtc_id; - } - } - - auto index = ++conn_type_count[conn->connector_type]; - - monitors.emplace_back(connector_t { - conn->connector_type, - crtc_id, - index, - conn->connection == DRM_MODE_CONNECTED, - }); - }); - - return monitors; - } - - file_t handleFD(std::uint32_t handle) { - file_t fb_fd; - - auto status = drmPrimeHandleToFD(fd.el, handle, 0 /* flags */, &fb_fd.el); - if(status) { - return {}; - } - - return fb_fd; - } - - std::vector> props(std::uint32_t id, std::uint32_t type) { - obj_prop_t obj_prop = drmModeObjectGetProperties(fd.el, id, type); - - std::vector> props; - props.reserve(obj_prop->count_props); - - for(auto x = 0; x < obj_prop->count_props; ++x) { - props.emplace_back(drmModeGetProperty(fd.el, obj_prop->props[x]), obj_prop->prop_values[x]); - } - - return props; - } - - std::vector> plane_props(std::uint32_t id) { - return props(id, DRM_MODE_OBJECT_PLANE); - } - - std::vector> crtc_props(std::uint32_t id) { - return props(id, DRM_MODE_OBJECT_CRTC); - } - - std::vector> connector_props(std::uint32_t id) { - return props(id, DRM_MODE_OBJECT_CONNECTOR); - } - - plane_t operator[](std::uint32_t index) { - return drmModeGetPlane(fd.el, plane_res->planes[index]); - } - - std::uint32_t count() { - return plane_res->count_planes; - } - - plane_it_t begin() const { - return plane_it_t { fd.el, plane_res->planes, plane_res->planes + plane_res->count_planes }; - } - - plane_it_t end() const { - return plane_it_t { fd.el, plane_res->planes + plane_res->count_planes }; - } - - - file_t fd; - plane_res_t plane_res; -}; - -std::map map_crtc_to_monitor(const std::vector &connectors) { - std::map result; - - for(auto &connector : connectors) { - result.emplace(connector.crtc_id, - monitor_t { - connector.type, - connector.index, - }); - } - - return result; -} - -struct kms_img_t : public img_t { - ~kms_img_t() override { - delete[] data; - data = nullptr; - } -}; - -void print(plane_t::pointer plane, fb_t::pointer fb, crtc_t::pointer crtc) { - if(crtc) { - BOOST_LOG(debug) << "crtc("sv << crtc->x << ", "sv << crtc->y << ')'; - BOOST_LOG(debug) << "crtc("sv << crtc->width << ", "sv << crtc->height << ')'; - BOOST_LOG(debug) << "plane->possible_crtcs == "sv << plane->possible_crtcs; - } - - BOOST_LOG(debug) - << "x("sv << plane->x - << ") y("sv << plane->y - << ") crtc_x("sv << plane->crtc_x - << ") crtc_y("sv << plane->crtc_y - << ") crtc_id("sv << plane->crtc_id - << ')'; - - BOOST_LOG(debug) - << "Resolution: "sv << fb->width << 'x' << fb->height - << ": Pitch: "sv << fb->pitches[0] - << ": Offset: "sv << fb->offsets[0]; - - std::stringstream ss; - - ss << "Format ["sv; - std::for_each_n(plane->formats, plane->count_formats - 1, [&ss](auto format) { - ss << util::view(format) << ", "sv; - }); - - ss << util::view(plane->formats[plane->count_formats - 1]) << ']'; - - BOOST_LOG(debug) << ss.str(); -} - -class display_t : public platf::display_t { -public: - display_t(mem_type_e mem_type) : platf::display_t(), mem_type { mem_type } {} - - int init(const std::string &display_name, const ::video::config_t &config) { - delay = std::chrono::nanoseconds { 1s } / config.framerate; - - int monitor_index = util::from_view(display_name); - int monitor = 0; - - fs::path card_dir { "/dev/dri"sv }; - for(auto &entry : fs::directory_iterator { card_dir }) { - auto file = entry.path().filename(); - - auto filestring = file.generic_u8string(); - if(filestring.size() < 4 || std::string_view { filestring }.substr(0, 4) != "card"sv) { - continue; - } - - kms::card_t card; - if(card.init(entry.path().c_str())) { - return {}; - } - - auto end = std::end(card); - for(auto plane = std::begin(card); plane != end; ++plane) { - if(card.is_cursor(plane->plane_id)) { - continue; - } - - if(monitor != monitor_index) { - ++monitor; - continue; - } - - auto fb = card.fb(plane.get()); - if(!fb) { - BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno); - return -1; - } - - if(!fb->handles[0]) { - BOOST_LOG(error) - << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+p sunshine]"sv; - return -1; - } - - for(int i = 0; i < 4; ++i) { - if(!fb->handles[i]) { - break; - } - - auto fb_fd = card.handleFD(fb->handles[i]); - if(fb_fd.el < 0) { - BOOST_LOG(error) << "Couldn't get primary file descriptor for Framebuffer ["sv << fb->fb_id << "]: "sv << strerror(errno); + if (!plane) { + BOOST_LOG(error) << "Couldn't get drm plane ["sv << (end - plane_p) << "]: "sv << strerror(errno); continue; } + + // If this plane is unused + if (plane->fb_id) { + this->plane = util::make_shared(plane.release()); + + // One last increment + ++plane_p; + break; + } } + } - BOOST_LOG(info) << "Found monitor for DRM screencasting"sv; + bool + eq(const plane_it_t &other) const { + return plane_p == other.plane_p; + } - // We need to find the correct /dev/dri/card{nr} to correlate the crtc_id with the monitor descriptor - auto pos = std::find_if(std::begin(card_descriptors), std::end(card_descriptors), [&](card_descriptor_t &cd) { - return cd.path == filestring; - }); + plane_t::pointer + get() { + return plane.get(); + } - if(pos == std::end(card_descriptors)) { - // This code path shouldn't happend, but it's there just in case. - // card_descriptors is part of the guesswork after all. - BOOST_LOG(error) << "Couldn't find ["sv << entry.path() << "]: This shouldn't have happened :/"sv; + int fd; + std::uint32_t *plane_p; + std::uint32_t *end; + + util::shared_t plane; + }; + + class card_t { + public: + using connector_interal_t = util::safe_ptr; + + int + init(const char *path) { + cap_sys_admin admin; + fd.el = open(path, O_RDWR); + + if (fd.el < 0) { + BOOST_LOG(error) << "Couldn't open: "sv << path << ": "sv << strerror(errno); return -1; } - //TODO: surf_sd = fb->to_sd(); + if (drmSetClientCap(fd.el, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)) { + BOOST_LOG(error) << "Couldn't expose some/all drm planes for card: "sv << path; + return -1; + } - auto crct = card.crtc(plane->crtc_id); - kms::print(plane.get(), fb.get(), crct.get()); + if (drmSetClientCap(fd.el, DRM_CLIENT_CAP_ATOMIC, 1)) { + BOOST_LOG(warning) << "Couldn't expose some properties for card: "sv << path; + } - img_width = fb->width; - img_height = fb->height; - img_offset_x = crct->x; - img_offset_y = crct->y; + plane_res.reset(drmModeGetPlaneResources(fd.el)); + if (!plane_res) { + BOOST_LOG(error) << "Couldn't get drm plane resources"sv; + return -1; + } - this->env_width = ::platf::kms::env_width; - this->env_height = ::platf::kms::env_height; + return 0; + } - auto monitor = pos->crtc_to_monitor.find(plane->crtc_id); - if(monitor != std::end(pos->crtc_to_monitor)) { - auto &viewport = monitor->second.viewport; + fb_t + fb(plane_t::pointer plane) { + cap_sys_admin admin; - width = viewport.width; - height = viewport.height; + auto fb2 = drmModeGetFB2(fd.el, plane->fb_id); + if (fb2) { + return std::make_unique(fb2); + } - switch(card.get_panel_orientation(plane->plane_id)) { - case DRM_MODE_ROTATE_270: - BOOST_LOG(debug) << "Detected panel orientation at 90, swapping width and height."; - width = viewport.height; - height = viewport.width; - break; - case DRM_MODE_ROTATE_90: - case DRM_MODE_ROTATE_180: - BOOST_LOG(warning) << "Panel orientation is unsupported, screen capture may not work correctly."; - break; + auto fb = drmModeGetFB(fd.el, plane->fb_id); + if (fb) { + return std::make_unique(fb); + } + + return nullptr; + } + + crtc_t + crtc(std::uint32_t id) { + return drmModeGetCrtc(fd.el, id); + } + + encoder_t + encoder(std::uint32_t id) { + return drmModeGetEncoder(fd.el, id); + } + + res_t + res() { + return drmModeGetResources(fd.el); + } + + bool + is_cursor(std::uint32_t plane_id) { + auto props = plane_props(plane_id); + for (auto &[prop, val] : props) { + if (prop->name == "type"sv) { + if (val == DRM_PLANE_TYPE_CURSOR) { + return true; + } + else { + return false; + } + } + } + + return false; + } + + std::uint32_t + get_panel_orientation(std::uint32_t plane_id) { + auto props = plane_props(plane_id); + for (auto &[prop, val] : props) { + if (prop->name == "rotation"sv) { + return val; + } + } + + BOOST_LOG(error) << "Failed to determine panel orientation, defaulting to landscape."; + return DRM_MODE_ROTATE_0; + } + + connector_interal_t + connector(std::uint32_t id) { + return drmModeGetConnector(fd.el, id); + } + + std::vector + monitors(conn_type_count_t &conn_type_count) { + auto resources = res(); + if (!resources) { + BOOST_LOG(error) << "Couldn't get connector resources"sv; + return {}; + } + + std::vector monitors; + std::for_each_n(resources->connectors, resources->count_connectors, [this, &conn_type_count, &monitors](std::uint32_t id) { + auto conn = connector(id); + + std::uint32_t crtc_id = 0; + + if (conn->encoder_id) { + auto enc = encoder(conn->encoder_id); + if (enc) { + crtc_id = enc->crtc_id; + } } - offset_x = viewport.offset_x; - offset_y = viewport.offset_y; + auto index = ++conn_type_count[conn->connector_type]; + + monitors.emplace_back(connector_t { + conn->connector_type, + crtc_id, + index, + conn->connection == DRM_MODE_CONNECTED, + }); + }); + + return monitors; + } + + file_t + handleFD(std::uint32_t handle) { + file_t fb_fd; + + auto status = drmPrimeHandleToFD(fd.el, handle, 0 /* flags */, &fb_fd.el); + if (status) { + return {}; } - // This code path shouldn't happend, but it's there just in case. - // crtc_to_monitor is part of the guesswork after all. - else { - BOOST_LOG(warning) << "Couldn't find crtc_id, this shouldn't have happened :\\"sv; - width = crct->width; - height = crct->height; - offset_x = crct->x; - offset_y = crct->y; + return fb_fd; + } + + std::vector> + props(std::uint32_t id, std::uint32_t type) { + obj_prop_t obj_prop = drmModeObjectGetProperties(fd.el, id, type); + + std::vector> props; + props.reserve(obj_prop->count_props); + + for (auto x = 0; x < obj_prop->count_props; ++x) { + props.emplace_back(drmModeGetProperty(fd.el, obj_prop->props[x]), obj_prop->prop_values[x]); } - this->card = std::move(card); - - plane_id = plane->plane_id; - - goto break_loop; - } - } - - // Neatly break from nested for loop - break_loop: - if(monitor != monitor_index) { - BOOST_LOG(error) << "Couldn't find monitor ["sv << monitor_index << ']'; - - return -1; - } - - cursor_opt = x11::cursor_t::make(); - - return 0; - } - - inline capture_e refresh(file_t *file, egl::surface_descriptor_t *sd) { - plane_t plane = drmModeGetPlane(card.fd.el, plane_id); - - auto fb = card.fb(plane.get()); - if(!fb) { - BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno); - return capture_e::error; - } - - if(!fb->handles[0]) { - BOOST_LOG(error) - << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+p sunshine]"sv; - return capture_e::error; - } - - for(int y = 0; y < 4; ++y) { - if(!fb->handles[y]) { - // setting sd->fds[y] to a negative value indicates that sd->offsets[y] and sd->pitches[y] - // are uninitialized and contain invalid values. - sd->fds[y] = -1; - // It's not clear whether there could still be valid handles left. - // So, continue anyway. - // TODO: Is this redundant? - continue; + return props; } - file[y] = card.handleFD(fb->handles[y]); - if(file[y].el < 0) { - BOOST_LOG(error) << "Couldn't get primary file descriptor for Framebuffer ["sv << fb->fb_id << "]: "sv << strerror(errno); - return capture_e::error; + std::vector> + plane_props(std::uint32_t id) { + return props(id, DRM_MODE_OBJECT_PLANE); } - sd->fds[y] = file[y].el; - sd->offsets[y] = fb->offsets[y]; - sd->pitches[y] = fb->pitches[y]; - } - - sd->width = fb->width; - sd->height = fb->height; - sd->modifier = fb->modifier; - sd->fourcc = fb->pixel_format; - - if( - fb->width != img_width || - fb->height != img_height) { - return capture_e::reinit; - } - - return capture_e::ok; - } - - mem_type_e mem_type; - - std::chrono::nanoseconds delay; - - int img_width, img_height; - int img_offset_x, img_offset_y; - - int plane_id; - - card_t card; - - std::optional cursor_opt; -}; - -class display_ram_t : public display_t { -public: - display_ram_t(mem_type_e mem_type) : display_t(mem_type) {} - - int init(const std::string &display_name, const ::video::config_t &config) { - if(!gbm::create_device) { - BOOST_LOG(warning) << "libgbm not initialized"sv; - return -1; - } - - if(display_t::init(display_name, config)) { - return -1; - } - - gbm.reset(gbm::create_device(card.fd.el)); - if(!gbm) { - BOOST_LOG(error) << "Couldn't create GBM device: ["sv << util::hex(eglGetError()).to_string_view() << ']'; - return -1; - } - - display = egl::make_display(gbm.get()); - if(!display) { - return -1; - } - - auto ctx_opt = egl::make_ctx(display.get()); - if(!ctx_opt) { - return -1; - } - - ctx = std::move(*ctx_opt); - - return 0; - } - - capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { - auto next_frame = std::chrono::steady_clock::now(); - - while(img) { - auto now = std::chrono::steady_clock::now(); - - if(next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::vector> + crtc_props(std::uint32_t id) { + return props(id, DRM_MODE_OBJECT_CRTC); } - while(next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); + + std::vector> + connector_props(std::uint32_t id) { + return props(id, DRM_MODE_OBJECT_CONNECTOR); } - next_frame = now + delay; - auto status = snapshot(img.get(), 1000ms, *cursor); - switch(status) { - case platf::capture_e::reinit: - case platf::capture_e::error: - return status; - case platf::capture_e::timeout: - img = snapshot_cb(img, false); - break; - case platf::capture_e::ok: - img = snapshot_cb(img, true); - break; - default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']'; - return status; + plane_t + operator[](std::uint32_t index) { + return drmModeGetPlane(fd.el, plane_res->planes[index]); } - } - return capture_e::ok; - } - - std::shared_ptr make_hwdevice(pix_fmt_e pix_fmt) override { - if(mem_type == mem_type_e::vaapi) { - return va::make_hwdevice(width, height, false); - } - - return std::make_shared(); - } - - capture_e snapshot(img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { - file_t fb_fd[4]; - - egl::surface_descriptor_t sd; - - auto status = refresh(fb_fd, &sd); - if(status != capture_e::ok) { - return status; - } - - auto rgb_opt = egl::import_source(display.get(), sd); - - if(!rgb_opt) { - return capture_e::error; - } - - auto &rgb = *rgb_opt; - - gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]); - - int w, h; - gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); - gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); - BOOST_LOG(debug) << "width and height: w "sv << w << " h "sv << h; - - gl::ctx.GetTextureSubImage(rgb->tex[0], 0, img_offset_x, img_offset_y, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out_base->height * img_out_base->row_pitch, img_out_base->data); - - if(cursor_opt && cursor) { - cursor_opt->blend(*img_out_base, img_offset_x, img_offset_y); - } - - return capture_e::ok; - } - - std::shared_ptr alloc_img() override { - auto img = std::make_shared(); - img->width = width; - img->height = height; - img->pixel_pitch = 4; - img->row_pitch = img->pixel_pitch * width; - img->data = new std::uint8_t[height * img->row_pitch]; - - return img; - } - - int dummy_img(platf::img_t *img) override { - return 0; - } - - gbm::gbm_t gbm; - egl::display_t display; - egl::ctx_t ctx; -}; - -class display_vram_t : public display_t { -public: - display_vram_t(mem_type_e mem_type) : display_t(mem_type) {} - - std::shared_ptr make_hwdevice(pix_fmt_e pix_fmt) override { - if(mem_type == mem_type_e::vaapi) { - return va::make_hwdevice(width, height, dup(card.fd.el), img_offset_x, img_offset_y, true); - } - - BOOST_LOG(error) << "Unsupported pixel format for egl::display_vram_t: "sv << platf::from_pix_fmt(pix_fmt); - return nullptr; - } - - std::shared_ptr alloc_img() override { - auto img = std::make_shared(); - - img->serial = std::numeric_limitsserial)>::max(); - img->data = nullptr; - img->pixel_pitch = 4; - - img->sequence = 0; - std::fill_n(img->sd.fds, 4, -1); - - return img; - } - - int dummy_img(platf::img_t *img) override { - return snapshot(img, 1s, false) != capture_e::ok; - } - - capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) { - auto next_frame = std::chrono::steady_clock::now(); - - while(img) { - auto now = std::chrono::steady_clock::now(); - - if(next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + std::uint32_t + count() { + return plane_res->count_planes; } - while(next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); + + plane_it_t + begin() const { + return plane_it_t { fd.el, plane_res->planes, plane_res->planes + plane_res->count_planes }; } - next_frame = now + delay; - auto status = snapshot(img.get(), 1000ms, *cursor); - switch(status) { - case platf::capture_e::reinit: - case platf::capture_e::error: - return status; - case platf::capture_e::timeout: - img = snapshot_cb(img, false); - break; - case platf::capture_e::ok: - img = snapshot_cb(img, true); - break; - default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']'; - return status; + plane_it_t + end() const { + return plane_it_t { fd.el, plane_res->planes + plane_res->count_planes }; } - } - return capture_e::ok; - } + file_t fd; + plane_res_t plane_res; + }; - capture_e snapshot(img_t *img_out_base, std::chrono::milliseconds /* timeout */, bool cursor) { - file_t fb_fd[4]; + std::map + map_crtc_to_monitor(const std::vector &connectors) { + std::map result; - auto img = (egl::img_descriptor_t *)img_out_base; - img->reset(); - - auto status = refresh(fb_fd, &img->sd); - if(status != capture_e::ok) { - return status; - } - - img->sequence = ++sequence; - - if(!cursor || !cursor_opt) { - img_out_base->data = nullptr; - - for(auto x = 0; x < 4; ++x) { - fb_fd[x].release(); + for (auto &connector : connectors) { + result.emplace(connector.crtc_id, + monitor_t { + connector.type, + connector.index, + }); } - return capture_e::ok; + + return result; } - cursor_opt->capture(*img); + struct kms_img_t: public img_t { + ~kms_img_t() override { + delete[] data; + data = nullptr; + } + }; - img->x -= offset_x; - img->y -= offset_y; + void + print(plane_t::pointer plane, fb_t::pointer fb, crtc_t::pointer crtc) { + if (crtc) { + BOOST_LOG(debug) << "crtc("sv << crtc->x << ", "sv << crtc->y << ')'; + BOOST_LOG(debug) << "crtc("sv << crtc->width << ", "sv << crtc->height << ')'; + BOOST_LOG(debug) << "plane->possible_crtcs == "sv << plane->possible_crtcs; + } - for(auto x = 0; x < 4; ++x) { - fb_fd[x].release(); + BOOST_LOG(debug) + << "x("sv << plane->x + << ") y("sv << plane->y + << ") crtc_x("sv << plane->crtc_x + << ") crtc_y("sv << plane->crtc_y + << ") crtc_id("sv << plane->crtc_id + << ')'; + + BOOST_LOG(debug) + << "Resolution: "sv << fb->width << 'x' << fb->height + << ": Pitch: "sv << fb->pitches[0] + << ": Offset: "sv << fb->offsets[0]; + + std::stringstream ss; + + ss << "Format ["sv; + std::for_each_n(plane->formats, plane->count_formats - 1, [&ss](auto format) { + ss << util::view(format) << ", "sv; + }); + + ss << util::view(plane->formats[plane->count_formats - 1]) << ']'; + + BOOST_LOG(debug) << ss.str(); } - return capture_e::ok; + + class display_t: public platf::display_t { + public: + display_t(mem_type_e mem_type): + platf::display_t(), mem_type { mem_type } {} + + int + init(const std::string &display_name, const ::video::config_t &config) { + delay = std::chrono::nanoseconds { 1s } / config.framerate; + + int monitor_index = util::from_view(display_name); + int monitor = 0; + + fs::path card_dir { "/dev/dri"sv }; + for (auto &entry : fs::directory_iterator { card_dir }) { + auto file = entry.path().filename(); + + auto filestring = file.generic_u8string(); + if (filestring.size() < 4 || std::string_view { filestring }.substr(0, 4) != "card"sv) { + continue; + } + + kms::card_t card; + if (card.init(entry.path().c_str())) { + return {}; + } + + auto end = std::end(card); + for (auto plane = std::begin(card); plane != end; ++plane) { + if (card.is_cursor(plane->plane_id)) { + continue; + } + + if (monitor != monitor_index) { + ++monitor; + continue; + } + + auto fb = card.fb(plane.get()); + if (!fb) { + BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno); + return -1; + } + + if (!fb->handles[0]) { + BOOST_LOG(error) + << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+p sunshine]"sv; + return -1; + } + + for (int i = 0; i < 4; ++i) { + if (!fb->handles[i]) { + break; + } + + auto fb_fd = card.handleFD(fb->handles[i]); + if (fb_fd.el < 0) { + BOOST_LOG(error) << "Couldn't get primary file descriptor for Framebuffer ["sv << fb->fb_id << "]: "sv << strerror(errno); + continue; + } + } + + BOOST_LOG(info) << "Found monitor for DRM screencasting"sv; + + // We need to find the correct /dev/dri/card{nr} to correlate the crtc_id with the monitor descriptor + auto pos = std::find_if(std::begin(card_descriptors), std::end(card_descriptors), [&](card_descriptor_t &cd) { + return cd.path == filestring; + }); + + if (pos == std::end(card_descriptors)) { + // This code path shouldn't happend, but it's there just in case. + // card_descriptors is part of the guesswork after all. + BOOST_LOG(error) << "Couldn't find ["sv << entry.path() << "]: This shouldn't have happened :/"sv; + return -1; + } + + //TODO: surf_sd = fb->to_sd(); + + auto crct = card.crtc(plane->crtc_id); + kms::print(plane.get(), fb.get(), crct.get()); + + img_width = fb->width; + img_height = fb->height; + img_offset_x = crct->x; + img_offset_y = crct->y; + + this->env_width = ::platf::kms::env_width; + this->env_height = ::platf::kms::env_height; + + auto monitor = pos->crtc_to_monitor.find(plane->crtc_id); + if (monitor != std::end(pos->crtc_to_monitor)) { + auto &viewport = monitor->second.viewport; + + width = viewport.width; + height = viewport.height; + + switch (card.get_panel_orientation(plane->plane_id)) { + case DRM_MODE_ROTATE_270: + BOOST_LOG(debug) << "Detected panel orientation at 90, swapping width and height."; + width = viewport.height; + height = viewport.width; + break; + case DRM_MODE_ROTATE_90: + case DRM_MODE_ROTATE_180: + BOOST_LOG(warning) << "Panel orientation is unsupported, screen capture may not work correctly."; + break; + } + + offset_x = viewport.offset_x; + offset_y = viewport.offset_y; + } + + // This code path shouldn't happend, but it's there just in case. + // crtc_to_monitor is part of the guesswork after all. + else { + BOOST_LOG(warning) << "Couldn't find crtc_id, this shouldn't have happened :\\"sv; + width = crct->width; + height = crct->height; + offset_x = crct->x; + offset_y = crct->y; + } + + this->card = std::move(card); + + plane_id = plane->plane_id; + + goto break_loop; + } + } + + // Neatly break from nested for loop + break_loop: + if (monitor != monitor_index) { + BOOST_LOG(error) << "Couldn't find monitor ["sv << monitor_index << ']'; + + return -1; + } + + cursor_opt = x11::cursor_t::make(); + + return 0; + } + + inline capture_e + refresh(file_t *file, egl::surface_descriptor_t *sd) { + plane_t plane = drmModeGetPlane(card.fd.el, plane_id); + + auto fb = card.fb(plane.get()); + if (!fb) { + BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno); + return capture_e::error; + } + + if (!fb->handles[0]) { + BOOST_LOG(error) + << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+p sunshine]"sv; + return capture_e::error; + } + + for (int y = 0; y < 4; ++y) { + if (!fb->handles[y]) { + // setting sd->fds[y] to a negative value indicates that sd->offsets[y] and sd->pitches[y] + // are uninitialized and contain invalid values. + sd->fds[y] = -1; + // It's not clear whether there could still be valid handles left. + // So, continue anyway. + // TODO: Is this redundant? + continue; + } + + file[y] = card.handleFD(fb->handles[y]); + if (file[y].el < 0) { + BOOST_LOG(error) << "Couldn't get primary file descriptor for Framebuffer ["sv << fb->fb_id << "]: "sv << strerror(errno); + return capture_e::error; + } + + sd->fds[y] = file[y].el; + sd->offsets[y] = fb->offsets[y]; + sd->pitches[y] = fb->pitches[y]; + } + + sd->width = fb->width; + sd->height = fb->height; + sd->modifier = fb->modifier; + sd->fourcc = fb->pixel_format; + + if ( + fb->width != img_width || + fb->height != img_height) { + return capture_e::reinit; + } + + return capture_e::ok; + } + + mem_type_e mem_type; + + std::chrono::nanoseconds delay; + + int img_width, img_height; + int img_offset_x, img_offset_y; + + int plane_id; + + card_t card; + + std::optional cursor_opt; + }; + + class display_ram_t: public display_t { + public: + display_ram_t(mem_type_e mem_type): + display_t(mem_type) {} + + int + init(const std::string &display_name, const ::video::config_t &config) { + if (!gbm::create_device) { + BOOST_LOG(warning) << "libgbm not initialized"sv; + return -1; + } + + if (display_t::init(display_name, config)) { + return -1; + } + + gbm.reset(gbm::create_device(card.fd.el)); + if (!gbm) { + BOOST_LOG(error) << "Couldn't create GBM device: ["sv << util::hex(eglGetError()).to_string_view() << ']'; + return -1; + } + + display = egl::make_display(gbm.get()); + if (!display) { + return -1; + } + + auto ctx_opt = egl::make_ctx(display.get()); + if (!ctx_opt) { + return -1; + } + + ctx = std::move(*ctx_opt); + + return 0; + } + + capture_e + capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { + auto next_frame = std::chrono::steady_clock::now(); + + while (img) { + auto now = std::chrono::steady_clock::now(); + + if (next_frame > now) { + std::this_thread::sleep_for((next_frame - now) / 3 * 2); + } + while (next_frame > now) { + std::this_thread::sleep_for(1ns); + now = std::chrono::steady_clock::now(); + } + next_frame = now + delay; + + auto status = snapshot(img.get(), 1000ms, *cursor); + switch (status) { + case platf::capture_e::reinit: + case platf::capture_e::error: + return status; + case platf::capture_e::timeout: + img = snapshot_cb(img, false); + break; + case platf::capture_e::ok: + img = snapshot_cb(img, true); + break; + default: + BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; + return status; + } + } + + return capture_e::ok; + } + + std::shared_ptr + make_hwdevice(pix_fmt_e pix_fmt) override { + if (mem_type == mem_type_e::vaapi) { + return va::make_hwdevice(width, height, false); + } + + return std::make_shared(); + } + + capture_e + snapshot(img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { + file_t fb_fd[4]; + + egl::surface_descriptor_t sd; + + auto status = refresh(fb_fd, &sd); + if (status != capture_e::ok) { + return status; + } + + auto rgb_opt = egl::import_source(display.get(), sd); + + if (!rgb_opt) { + return capture_e::error; + } + + auto &rgb = *rgb_opt; + + gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]); + + int w, h; + gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); + gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); + BOOST_LOG(debug) << "width and height: w "sv << w << " h "sv << h; + + gl::ctx.GetTextureSubImage(rgb->tex[0], 0, img_offset_x, img_offset_y, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out_base->height * img_out_base->row_pitch, img_out_base->data); + + if (cursor_opt && cursor) { + cursor_opt->blend(*img_out_base, img_offset_x, img_offset_y); + } + + return capture_e::ok; + } + + std::shared_ptr + alloc_img() override { + auto img = std::make_shared(); + img->width = width; + img->height = height; + img->pixel_pitch = 4; + img->row_pitch = img->pixel_pitch * width; + img->data = new std::uint8_t[height * img->row_pitch]; + + return img; + } + + int + dummy_img(platf::img_t *img) override { + return 0; + } + + gbm::gbm_t gbm; + egl::display_t display; + egl::ctx_t ctx; + }; + + class display_vram_t: public display_t { + public: + display_vram_t(mem_type_e mem_type): + display_t(mem_type) {} + + std::shared_ptr + make_hwdevice(pix_fmt_e pix_fmt) override { + if (mem_type == mem_type_e::vaapi) { + return va::make_hwdevice(width, height, dup(card.fd.el), img_offset_x, img_offset_y, true); + } + + BOOST_LOG(error) << "Unsupported pixel format for egl::display_vram_t: "sv << platf::from_pix_fmt(pix_fmt); + return nullptr; + } + + std::shared_ptr + alloc_img() override { + auto img = std::make_shared(); + + img->serial = std::numeric_limitsserial)>::max(); + img->data = nullptr; + img->pixel_pitch = 4; + + img->sequence = 0; + std::fill_n(img->sd.fds, 4, -1); + + return img; + } + + int + dummy_img(platf::img_t *img) override { + return snapshot(img, 1s, false) != capture_e::ok; + } + + capture_e + capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) { + auto next_frame = std::chrono::steady_clock::now(); + + while (img) { + auto now = std::chrono::steady_clock::now(); + + if (next_frame > now) { + std::this_thread::sleep_for((next_frame - now) / 3 * 2); + } + while (next_frame > now) { + std::this_thread::sleep_for(1ns); + now = std::chrono::steady_clock::now(); + } + next_frame = now + delay; + + auto status = snapshot(img.get(), 1000ms, *cursor); + switch (status) { + case platf::capture_e::reinit: + case platf::capture_e::error: + return status; + case platf::capture_e::timeout: + img = snapshot_cb(img, false); + break; + case platf::capture_e::ok: + img = snapshot_cb(img, true); + break; + default: + BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; + return status; + } + } + + return capture_e::ok; + } + + capture_e + snapshot(img_t *img_out_base, std::chrono::milliseconds /* timeout */, bool cursor) { + file_t fb_fd[4]; + + auto img = (egl::img_descriptor_t *) img_out_base; + img->reset(); + + auto status = refresh(fb_fd, &img->sd); + if (status != capture_e::ok) { + return status; + } + + img->sequence = ++sequence; + + if (!cursor || !cursor_opt) { + img_out_base->data = nullptr; + + for (auto x = 0; x < 4; ++x) { + fb_fd[x].release(); + } + return capture_e::ok; + } + + cursor_opt->capture(*img); + + img->x -= offset_x; + img->y -= offset_y; + + for (auto x = 0; x < 4; ++x) { + fb_fd[x].release(); + } + return capture_e::ok; + } + + int + init(const std::string &display_name, const ::video::config_t &config) { + if (display_t::init(display_name, config)) { + return -1; + } + + if (!va::validate(card.fd.el)) { + BOOST_LOG(warning) << "Monitor "sv << display_name << " doesn't support hardware encoding. Reverting back to GPU -> RAM -> GPU"sv; + return -1; + } + + sequence = 0; + + return 0; + } + + std::uint64_t sequence; + }; + + } // namespace kms + + std::shared_ptr + kms_display(mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { + if (hwdevice_type == mem_type_e::vaapi) { + auto disp = std::make_shared(hwdevice_type); + + if (!disp->init(display_name, config)) { + return disp; + } + + // In the case of failure, attempt the old method for VAAPI + } + + auto disp = std::make_shared(hwdevice_type); + + if (disp->init(display_name, config)) { + return nullptr; + } + + return disp; } - int init(const std::string &display_name, const ::video::config_t &config) { - if(display_t::init(display_name, config)) { - return -1; - } - - if(!va::validate(card.fd.el)) { - BOOST_LOG(warning) << "Monitor "sv << display_name << " doesn't support hardware encoding. Reverting back to GPU -> RAM -> GPU"sv; - return -1; - } - - sequence = 0; - - return 0; - } - - std::uint64_t sequence; -}; - -} // namespace kms - -std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { - if(hwdevice_type == mem_type_e::vaapi) { - auto disp = std::make_shared(hwdevice_type); - - if(!disp->init(display_name, config)) { - return disp; - } - - // In the case of failure, attempt the old method for VAAPI - } - - auto disp = std::make_shared(hwdevice_type); - - if(disp->init(display_name, config)) { - return nullptr; - } - - return disp; -} - - -/** + /** * On Wayland, it's not possible to determine the position of the monitor on the desktop with KMS. * Wayland does allow applications to query attached monitors on the desktop, * however, the naming scheme is not standardized across implementations. @@ -939,164 +980,166 @@ std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::stri * * This is an ugly hack :( */ -void correlate_to_wayland(std::vector &cds) { - auto monitors = wl::monitors(); + void + correlate_to_wayland(std::vector &cds) { + auto monitors = wl::monitors(); - for(auto &monitor : monitors) { - std::string_view name = monitor->name; + for (auto &monitor : monitors) { + std::string_view name = monitor->name; - BOOST_LOG(info) << name << ": "sv << monitor->description; + BOOST_LOG(info) << name << ": "sv << monitor->description; - // Try to convert names in the format: - // {type}-{index} - // {index} is n'th occurence of {type} - auto index_begin = name.find_last_of('-'); + // Try to convert names in the format: + // {type}-{index} + // {index} is n'th occurence of {type} + auto index_begin = name.find_last_of('-'); - std::uint32_t index; - if(index_begin == std::string_view::npos) { - index = 1; - } - else { - index = std::max(1, util::from_view(name.substr(index_begin + 1))); - } + std::uint32_t index; + if (index_begin == std::string_view::npos) { + index = 1; + } + else { + index = std::max(1, util::from_view(name.substr(index_begin + 1))); + } - auto type = kms::from_view(name.substr(0, index_begin)); + auto type = kms::from_view(name.substr(0, index_begin)); - for(auto &card_descriptor : cds) { - for(auto &[_, monitor_descriptor] : card_descriptor.crtc_to_monitor) { - if(monitor_descriptor.index == index && monitor_descriptor.type == type) { - monitor_descriptor.viewport.offset_x = monitor->viewport.offset_x; - monitor_descriptor.viewport.offset_y = monitor->viewport.offset_y; + for (auto &card_descriptor : cds) { + for (auto &[_, monitor_descriptor] : card_descriptor.crtc_to_monitor) { + if (monitor_descriptor.index == index && monitor_descriptor.type == type) { + monitor_descriptor.viewport.offset_x = monitor->viewport.offset_x; + monitor_descriptor.viewport.offset_y = monitor->viewport.offset_y; - // A sanity check, it's guesswork after all. - if( - monitor_descriptor.viewport.width != monitor->viewport.width || - monitor_descriptor.viewport.height != monitor->viewport.height) { - BOOST_LOG(warning) - << "Mismatch on expected Resolution compared to actual resolution: "sv - << monitor_descriptor.viewport.width << 'x' << monitor_descriptor.viewport.height - << " vs "sv - << monitor->viewport.width << 'x' << monitor->viewport.height; + // A sanity check, it's guesswork after all. + if ( + monitor_descriptor.viewport.width != monitor->viewport.width || + monitor_descriptor.viewport.height != monitor->viewport.height) { + BOOST_LOG(warning) + << "Mismatch on expected Resolution compared to actual resolution: "sv + << monitor_descriptor.viewport.width << 'x' << monitor_descriptor.viewport.height + << " vs "sv + << monitor->viewport.width << 'x' << monitor->viewport.height; + } + + goto break_for_loop; } - - goto break_for_loop; } } + break_for_loop: + + BOOST_LOG(verbose) << "Reduced to name: "sv << name << ": "sv << index; } - break_for_loop: - - BOOST_LOG(verbose) << "Reduced to name: "sv << name << ": "sv << index; - } -} - -// A list of names of displays accepted as display_name -std::vector kms_display_names() { - int count = 0; - - if(!fs::exists("/dev/dri")) { - BOOST_LOG(warning) << "Couldn't find /dev/dri, kmsgrab won't be enabled"sv; - return {}; } - if(!gbm::create_device) { - BOOST_LOG(warning) << "libgbm not initialized"sv; - return {}; - } + // A list of names of displays accepted as display_name + std::vector + kms_display_names() { + int count = 0; - kms::conn_type_count_t conn_type_count; - - std::vector cds; - std::vector display_names; - - fs::path card_dir { "/dev/dri"sv }; - for(auto &entry : fs::directory_iterator { card_dir }) { - auto file = entry.path().filename(); - - auto filestring = file.generic_u8string(); - if(std::string_view { filestring }.substr(0, 4) != "card"sv) { - continue; - } - - kms::card_t card; - if(card.init(entry.path().c_str())) { + if (!fs::exists("/dev/dri")) { + BOOST_LOG(warning) << "Couldn't find /dev/dri, kmsgrab won't be enabled"sv; return {}; } - auto crtc_to_monitor = kms::map_crtc_to_monitor(card.monitors(conn_type_count)); + if (!gbm::create_device) { + BOOST_LOG(warning) << "libgbm not initialized"sv; + return {}; + } - auto end = std::end(card); - for(auto plane = std::begin(card); plane != end; ++plane) { - auto fb = card.fb(plane.get()); - if(!fb) { - BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno); + kms::conn_type_count_t conn_type_count; + + std::vector cds; + std::vector display_names; + + fs::path card_dir { "/dev/dri"sv }; + for (auto &entry : fs::directory_iterator { card_dir }) { + auto file = entry.path().filename(); + + auto filestring = file.generic_u8string(); + if (std::string_view { filestring }.substr(0, 4) != "card"sv) { continue; } - if(!fb->handles[0]) { - BOOST_LOG(error) - << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+p sunshine]"sv; - break; - } - - if(card.is_cursor(plane->plane_id)) { - continue; - } - - // This appears to return the offset of the monitor - auto crtc = card.crtc(plane->crtc_id); - if(!crtc) { - BOOST_LOG(error) << "Couldn't get crtc info: "sv << strerror(errno); + kms::card_t card; + if (card.init(entry.path().c_str())) { return {}; } - auto it = crtc_to_monitor.find(plane->crtc_id); - if(it != std::end(crtc_to_monitor)) { - it->second.viewport = platf::touch_port_t { - (int)crtc->x, - (int)crtc->y, - (int)crtc->width, - (int)crtc->height, - }; + auto crtc_to_monitor = kms::map_crtc_to_monitor(card.monitors(conn_type_count)); + + auto end = std::end(card); + for (auto plane = std::begin(card); plane != end; ++plane) { + auto fb = card.fb(plane.get()); + if (!fb) { + BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno); + continue; + } + + if (!fb->handles[0]) { + BOOST_LOG(error) + << "Couldn't get handle for DRM Framebuffer ["sv << plane->fb_id << "]: Possibly not permitted: do [sudo setcap cap_sys_admin+p sunshine]"sv; + break; + } + + if (card.is_cursor(plane->plane_id)) { + continue; + } + + // This appears to return the offset of the monitor + auto crtc = card.crtc(plane->crtc_id); + if (!crtc) { + BOOST_LOG(error) << "Couldn't get crtc info: "sv << strerror(errno); + return {}; + } + + auto it = crtc_to_monitor.find(plane->crtc_id); + if (it != std::end(crtc_to_monitor)) { + it->second.viewport = platf::touch_port_t { + (int) crtc->x, + (int) crtc->y, + (int) crtc->width, + (int) crtc->height, + }; + } + + kms::env_width = std::max(kms::env_width, (int) (crtc->x + crtc->width)); + kms::env_height = std::max(kms::env_height, (int) (crtc->y + crtc->height)); + + kms::print(plane.get(), fb.get(), crtc.get()); + + display_names.emplace_back(std::to_string(count++)); } - kms::env_width = std::max(kms::env_width, (int)(crtc->x + crtc->width)); - kms::env_height = std::max(kms::env_height, (int)(crtc->y + crtc->height)); - - kms::print(plane.get(), fb.get(), crtc.get()); - - display_names.emplace_back(std::to_string(count++)); + cds.emplace_back(kms::card_descriptor_t { + std::move(file), + std::move(crtc_to_monitor), + }); } - cds.emplace_back(kms::card_descriptor_t { - std::move(file), - std::move(crtc_to_monitor), - }); - } - - if(!wl::init()) { - correlate_to_wayland(cds); - } - - // Deduce the full virtual desktop size - kms::env_width = 0; - kms::env_height = 0; - - for(auto &card_descriptor : cds) { - for(auto &[_, monitor_descriptor] : card_descriptor.crtc_to_monitor) { - BOOST_LOG(debug) << "Monitor description"sv; - BOOST_LOG(debug) << "Resolution: "sv << monitor_descriptor.viewport.width << 'x' << monitor_descriptor.viewport.height; - BOOST_LOG(debug) << "Offset: "sv << monitor_descriptor.viewport.offset_x << 'x' << monitor_descriptor.viewport.offset_y; - - kms::env_width = std::max(kms::env_width, (int)(monitor_descriptor.viewport.offset_x + monitor_descriptor.viewport.width)); - kms::env_height = std::max(kms::env_height, (int)(monitor_descriptor.viewport.offset_y + monitor_descriptor.viewport.height)); + if (!wl::init()) { + correlate_to_wayland(cds); } + + // Deduce the full virtual desktop size + kms::env_width = 0; + kms::env_height = 0; + + for (auto &card_descriptor : cds) { + for (auto &[_, monitor_descriptor] : card_descriptor.crtc_to_monitor) { + BOOST_LOG(debug) << "Monitor description"sv; + BOOST_LOG(debug) << "Resolution: "sv << monitor_descriptor.viewport.width << 'x' << monitor_descriptor.viewport.height; + BOOST_LOG(debug) << "Offset: "sv << monitor_descriptor.viewport.offset_x << 'x' << monitor_descriptor.viewport.offset_y; + + kms::env_width = std::max(kms::env_width, (int) (monitor_descriptor.viewport.offset_x + monitor_descriptor.viewport.width)); + kms::env_height = std::max(kms::env_height, (int) (monitor_descriptor.viewport.offset_y + monitor_descriptor.viewport.height)); + } + } + + BOOST_LOG(debug) << "Desktop resolution: "sv << kms::env_width << 'x' << kms::env_height; + + kms::card_descriptors = std::move(cds); + + return display_names; } - BOOST_LOG(debug) << "Desktop resolution: "sv << kms::env_width << 'x' << kms::env_height; - - kms::card_descriptors = std::move(cds); - - return display_names; -} - -} // namespace platf +} // namespace platf diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index 01a7bf60..899e2a72 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -25,9 +25,9 @@ #include "vaapi.h" #ifdef __GNUC__ -#define SUNSHINE_GNUC_EXTENSION __extension__ + #define SUNSHINE_GNUC_EXTENSION __extension__ #else -#define SUNSHINE_GNUC_EXTENSION + #define SUNSHINE_GNUC_EXTENSION #endif using namespace std::literals; @@ -37,538 +37,569 @@ namespace bp = boost::process; window_system_e window_system; namespace dyn { -void *handle(const std::vector &libs) { - void *handle; + void * + handle(const std::vector &libs) { + void *handle; - for(auto lib : libs) { - handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL); - if(handle) { - return handle; - } - } - - std::stringstream ss; - ss << "Couldn't find any of the following libraries: ["sv << libs.front(); - std::for_each(std::begin(libs) + 1, std::end(libs), [&](auto lib) { - ss << ", "sv << lib; - }); - - ss << ']'; - - BOOST_LOG(error) << ss.str(); - - return nullptr; -} - -int load(void *handle, const std::vector> &funcs, bool strict) { - int err = 0; - for(auto &func : funcs) { - TUPLE_2D_REF(fn, name, func); - - *fn = SUNSHINE_GNUC_EXTENSION(apiproc) dlsym(handle, name); - - if(!*fn && strict) { - BOOST_LOG(error) << "Couldn't find function: "sv << name; - - err = -1; - } - } - - return err; -} -} // namespace dyn -namespace platf { -using ifaddr_t = util::safe_ptr; - -ifaddr_t get_ifaddrs() { - ifaddrs *p { nullptr }; - - getifaddrs(&p); - - return ifaddr_t { p }; -} - -fs::path appdata() { - const char *homedir; - if((homedir = getenv("HOME")) == nullptr) { - homedir = getpwuid(geteuid())->pw_dir; - } - - return fs::path { homedir } / ".config/sunshine"sv; -} - -std::string from_sockaddr(const sockaddr *const ip_addr) { - char data[INET6_ADDRSTRLEN]; - - auto family = ip_addr->sa_family; - if(family == AF_INET6) { - inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data, - INET6_ADDRSTRLEN); - } - - if(family == AF_INET) { - inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data, - INET_ADDRSTRLEN); - } - - return std::string { data }; -} - -std::pair from_sockaddr_ex(const sockaddr *const ip_addr) { - char data[INET6_ADDRSTRLEN]; - - auto family = ip_addr->sa_family; - std::uint16_t port; - if(family == AF_INET6) { - inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data, - INET6_ADDRSTRLEN); - port = ((sockaddr_in6 *)ip_addr)->sin6_port; - } - - if(family == AF_INET) { - inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data, - INET_ADDRSTRLEN); - port = ((sockaddr_in *)ip_addr)->sin_port; - } - - return { port, std::string { data } }; -} - -std::string get_mac_address(const std::string_view &address) { - auto ifaddrs = get_ifaddrs(); - for(auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) { - if(pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) { - std::ifstream mac_file("/sys/class/net/"s + pos->ifa_name + "/address"); - if(mac_file.good()) { - std::string mac_address; - std::getline(mac_file, mac_address); - return mac_address; + for (auto lib : libs) { + handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL); + if (handle) { + return handle; } } + + std::stringstream ss; + ss << "Couldn't find any of the following libraries: ["sv << libs.front(); + std::for_each(std::begin(libs) + 1, std::end(libs), [&](auto lib) { + ss << ", "sv << lib; + }); + + ss << ']'; + + BOOST_LOG(error) << ss.str(); + + return nullptr; } - BOOST_LOG(warning) << "Unable to find MAC address for "sv << address; - return "00:00:00:00:00:00"s; -} + int + load(void *handle, const std::vector> &funcs, bool strict) { + int err = 0; + for (auto &func : funcs) { + TUPLE_2D_REF(fn, name, func); -bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv; - if(!group) { - if(!file) { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); + *fn = SUNSHINE_GNUC_EXTENSION(apiproc) dlsym(handle, name); + + if (!*fn && strict) { + BOOST_LOG(error) << "Couldn't find function: "sv << name; + + err = -1; + } } - else { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec); + + return err; + } +} // namespace dyn +namespace platf { + using ifaddr_t = util::safe_ptr; + + ifaddr_t + get_ifaddrs() { + ifaddrs *p { nullptr }; + + getifaddrs(&p); + + return ifaddr_t { p }; + } + + fs::path + appdata() { + const char *homedir; + if ((homedir = getenv("HOME")) == nullptr) { + homedir = getpwuid(geteuid())->pw_dir; } + + return fs::path { homedir } / ".config/sunshine"sv; } - else { - if(!file) { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec, *group); + + std::string + from_sockaddr(const sockaddr *const ip_addr) { + char data[INET6_ADDRSTRLEN]; + + auto family = ip_addr->sa_family; + if (family == AF_INET6) { + inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, + INET6_ADDRSTRLEN); } - else { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec, *group); + + if (family == AF_INET) { + inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, + INET_ADDRSTRLEN); } - } -} -void adjust_thread_priority(thread_priority_e priority) { - // Unimplemented -} - -void streaming_will_start() { - // Nothing to do -} - -void streaming_will_stop() { - // Nothing to do -} - -bool restart_supported() { - // Restart not supported yet - return false; -} - -bool restart() { - // Restart not supported yet - return false; -} - -bool send_batch(batched_send_info_t &send_info) { - auto sockfd = (int)send_info.native_socket; - - // Convert the target address into a sockaddr - struct sockaddr_in saddr_v4 = {}; - struct sockaddr_in6 saddr_v6 = {}; - struct sockaddr *addr; - socklen_t addr_len; - if(send_info.target_address.is_v6()) { - auto address_v6 = send_info.target_address.to_v6(); - - saddr_v6.sin6_family = AF_INET6; - saddr_v6.sin6_port = htons(send_info.target_port); - saddr_v6.sin6_scope_id = address_v6.scope_id(); - - auto addr_bytes = address_v6.to_bytes(); - memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr)); - - addr = (struct sockaddr *)&saddr_v6; - addr_len = sizeof(saddr_v6); - } - else { - auto address_v4 = send_info.target_address.to_v4(); - - saddr_v4.sin_family = AF_INET; - saddr_v4.sin_port = htons(send_info.target_port); - - auto addr_bytes = address_v4.to_bytes(); - memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr)); - - addr = (struct sockaddr *)&saddr_v4; - addr_len = sizeof(saddr_v4); + return std::string { data }; } -#ifdef UDP_SEGMENT - { - struct msghdr msg = {}; - struct iovec iov = {}; - union { - char buf[CMSG_SPACE(sizeof(uint16_t))]; - struct cmsghdr alignment; - } cmbuf; + std::pair + from_sockaddr_ex(const sockaddr *const ip_addr) { + char data[INET6_ADDRSTRLEN]; - // UDP GSO on Linux currently only supports sending 64K or 64 segments at a time - size_t seg_index = 0; - const size_t seg_max = 65536 / 1500; - while(seg_index < send_info.block_count) { - iov.iov_base = (void *)&send_info.buffer[seg_index * send_info.block_size]; - iov.iov_len = send_info.block_size * std::min(send_info.block_count - seg_index, seg_max); + auto family = ip_addr->sa_family; + std::uint16_t port; + if (family == AF_INET6) { + inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, + INET6_ADDRSTRLEN); + port = ((sockaddr_in6 *) ip_addr)->sin6_port; + } - msg.msg_name = addr; - msg.msg_namelen = addr_len; - msg.msg_iov = &iov; - msg.msg_iovlen = 1; + if (family == AF_INET) { + inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, + INET_ADDRSTRLEN); + port = ((sockaddr_in *) ip_addr)->sin_port; + } - // We should not use GSO if the data is <= one full block size - if(iov.iov_len > send_info.block_size) { - msg.msg_control = cmbuf.buf; - msg.msg_controllen = CMSG_SPACE(sizeof(uint16_t)); + return { port, std::string { data } }; + } - // Enable GSO to perform segmentation of our buffer for us - auto cm = CMSG_FIRSTHDR(&msg); - cm->cmsg_level = SOL_UDP; - cm->cmsg_type = UDP_SEGMENT; - cm->cmsg_len = CMSG_LEN(sizeof(uint16_t)); - *((uint16_t *)CMSG_DATA(cm)) = send_info.block_size; + std::string + get_mac_address(const std::string_view &address) { + auto ifaddrs = get_ifaddrs(); + for (auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) { + if (pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) { + std::ifstream mac_file("/sys/class/net/"s + pos->ifa_name + "/address"); + if (mac_file.good()) { + std::string mac_address; + std::getline(mac_file, mac_address); + return mac_address; + } + } + } + + BOOST_LOG(warning) << "Unable to find MAC address for "sv << address; + return "00:00:00:00:00:00"s; + } + + bp::child + run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { + BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv; + if (!group) { + if (!file) { + return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); } else { - msg.msg_control = nullptr; - msg.msg_controllen = 0; + return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec); } + } + else { + if (!file) { + return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec, *group); + } + else { + return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec, *group); + } + } + } - // This will fail if GSO is not available, so we will fall back to non-GSO if - // it's the first sendmsg() call. On subsequent calls, we will treat errors as - // actual failures and return to the caller. - auto bytes_sent = sendmsg(sockfd, &msg, 0); - if(bytes_sent < 0) { - // If there's no send buffer space, wait for some to be available - if(errno == EAGAIN) { - struct pollfd pfd; + void + adjust_thread_priority(thread_priority_e priority) { + // Unimplemented + } - pfd.fd = sockfd; - pfd.events = POLLOUT; + void + streaming_will_start() { + // Nothing to do + } - if(poll(&pfd, 1, -1) != 1) { - BOOST_LOG(warning) << "poll() failed: "sv << errno; - break; - } + void + streaming_will_stop() { + // Nothing to do + } - // Try to send again - continue; + bool + restart_supported() { + // Restart not supported yet + return false; + } + + bool + restart() { + // Restart not supported yet + return false; + } + + bool + send_batch(batched_send_info_t &send_info) { + auto sockfd = (int) send_info.native_socket; + + // Convert the target address into a sockaddr + struct sockaddr_in saddr_v4 = {}; + struct sockaddr_in6 saddr_v6 = {}; + struct sockaddr *addr; + socklen_t addr_len; + if (send_info.target_address.is_v6()) { + auto address_v6 = send_info.target_address.to_v6(); + + saddr_v6.sin6_family = AF_INET6; + saddr_v6.sin6_port = htons(send_info.target_port); + saddr_v6.sin6_scope_id = address_v6.scope_id(); + + auto addr_bytes = address_v6.to_bytes(); + memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr)); + + addr = (struct sockaddr *) &saddr_v6; + addr_len = sizeof(saddr_v6); + } + else { + auto address_v4 = send_info.target_address.to_v4(); + + saddr_v4.sin_family = AF_INET; + saddr_v4.sin_port = htons(send_info.target_port); + + auto addr_bytes = address_v4.to_bytes(); + memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr)); + + addr = (struct sockaddr *) &saddr_v4; + addr_len = sizeof(saddr_v4); + } + +#ifdef UDP_SEGMENT + { + struct msghdr msg = {}; + struct iovec iov = {}; + union { + char buf[CMSG_SPACE(sizeof(uint16_t))]; + struct cmsghdr alignment; + } cmbuf; + + // UDP GSO on Linux currently only supports sending 64K or 64 segments at a time + size_t seg_index = 0; + const size_t seg_max = 65536 / 1500; + while (seg_index < send_info.block_count) { + iov.iov_base = (void *) &send_info.buffer[seg_index * send_info.block_size]; + iov.iov_len = send_info.block_size * std::min(send_info.block_count - seg_index, seg_max); + + msg.msg_name = addr; + msg.msg_namelen = addr_len; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + // We should not use GSO if the data is <= one full block size + if (iov.iov_len > send_info.block_size) { + msg.msg_control = cmbuf.buf; + msg.msg_controllen = CMSG_SPACE(sizeof(uint16_t)); + + // Enable GSO to perform segmentation of our buffer for us + auto cm = CMSG_FIRSTHDR(&msg); + cm->cmsg_level = SOL_UDP; + cm->cmsg_type = UDP_SEGMENT; + cm->cmsg_len = CMSG_LEN(sizeof(uint16_t)); + *((uint16_t *) CMSG_DATA(cm)) = send_info.block_size; + } + else { + msg.msg_control = nullptr; + msg.msg_controllen = 0; } + // This will fail if GSO is not available, so we will fall back to non-GSO if + // it's the first sendmsg() call. On subsequent calls, we will treat errors as + // actual failures and return to the caller. + auto bytes_sent = sendmsg(sockfd, &msg, 0); + if (bytes_sent < 0) { + // If there's no send buffer space, wait for some to be available + if (errno == EAGAIN) { + struct pollfd pfd; + + pfd.fd = sockfd; + pfd.events = POLLOUT; + + if (poll(&pfd, 1, -1) != 1) { + BOOST_LOG(warning) << "poll() failed: "sv << errno; + break; + } + + // Try to send again + continue; + } + + break; + } + + seg_index += bytes_sent / send_info.block_size; + } + + // If we sent something, return the status and don't fall back to the non-GSO path. + if (seg_index != 0) { + return seg_index >= send_info.block_count; + } + } +#endif + + { + // If GSO is not supported, use sendmmsg() instead. + struct mmsghdr msgs[send_info.block_count]; + struct iovec iovs[send_info.block_count]; + for (size_t i = 0; i < send_info.block_count; i++) { + iovs[i] = {}; + iovs[i].iov_base = (void *) &send_info.buffer[i * send_info.block_size]; + iovs[i].iov_len = send_info.block_size; + + msgs[i] = {}; + msgs[i].msg_hdr.msg_name = addr; + msgs[i].msg_hdr.msg_namelen = addr_len; + msgs[i].msg_hdr.msg_iov = &iovs[i]; + msgs[i].msg_hdr.msg_iovlen = 1; + } + + // Call sendmmsg() until all messages are sent + size_t blocks_sent = 0; + while (blocks_sent < send_info.block_count) { + int msgs_sent = sendmmsg(sockfd, &msgs[blocks_sent], send_info.block_count - blocks_sent, 0); + if (msgs_sent < 0) { + // If there's no send buffer space, wait for some to be available + if (errno == EAGAIN) { + struct pollfd pfd; + + pfd.fd = sockfd; + pfd.events = POLLOUT; + + if (poll(&pfd, 1, -1) != 1) { + BOOST_LOG(warning) << "poll() failed: "sv << errno; + break; + } + + // Try to send again + continue; + } + + BOOST_LOG(warning) << "sendmmsg() failed: "sv << errno; + return false; + } + + blocks_sent += msgs_sent; + } + + return true; + } + } + + class qos_t: public deinit_t { + public: + qos_t(int sockfd, int level, int option): + sockfd(sockfd), level(level), option(option) {} + + virtual ~qos_t() { + int reset_val = -1; + if (setsockopt(sockfd, level, option, &reset_val, sizeof(reset_val)) < 0) { + BOOST_LOG(warning) << "Failed to reset IP TOS: "sv << errno; + } + } + + private: + int sockfd; + int level; + int option; + }; + + std::unique_ptr + enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { + int sockfd = (int) native_socket; + + int level; + int option; + if (address.is_v6()) { + level = SOL_IPV6; + option = IPV6_TCLASS; + } + else { + level = SOL_IP; + option = IP_TOS; + } + + // The specific DSCP values here are chosen to be consistent with Windows + int dscp; + switch (data_type) { + case qos_data_type_e::video: + dscp = 40; break; - } - - seg_index += bytes_sent / send_info.block_size; + case qos_data_type_e::audio: + dscp = 56; + break; + default: + BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; + return nullptr; } - // If we sent something, return the status and don't fall back to the non-GSO path. - if(seg_index != 0) { - return seg_index >= send_info.block_count; + // Shift to put the DSCP value in the correct position in the TOS field + dscp <<= 2; + + if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) < 0) { + return nullptr; } + + return std::make_unique(sockfd, level, option); + } + + namespace source { + enum source_e : std::size_t { +#ifdef SUNSHINE_BUILD_CUDA + NVFBC, +#endif +#ifdef SUNSHINE_BUILD_WAYLAND + WAYLAND, +#endif +#ifdef SUNSHINE_BUILD_DRM + KMS, +#endif +#ifdef SUNSHINE_BUILD_X11 + X11, +#endif + MAX_FLAGS + }; + } // namespace source + + static std::bitset sources; + +#ifdef SUNSHINE_BUILD_CUDA + std::vector + nvfbc_display_names(); + std::shared_ptr + nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); + + bool + verify_nvfbc() { + return !nvfbc_display_names().empty(); } #endif - { - // If GSO is not supported, use sendmmsg() instead. - struct mmsghdr msgs[send_info.block_count]; - struct iovec iovs[send_info.block_count]; - for(size_t i = 0; i < send_info.block_count; i++) { - iovs[i] = {}; - iovs[i].iov_base = (void *)&send_info.buffer[i * send_info.block_size]; - iovs[i].iov_len = send_info.block_size; +#ifdef SUNSHINE_BUILD_WAYLAND + std::vector + wl_display_names(); + std::shared_ptr + wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); - msgs[i] = {}; - msgs[i].msg_hdr.msg_name = addr; - msgs[i].msg_hdr.msg_namelen = addr_len; - msgs[i].msg_hdr.msg_iov = &iovs[i]; - msgs[i].msg_hdr.msg_iovlen = 1; + bool + verify_wl() { + return window_system == window_system_e::WAYLAND && !wl_display_names().empty(); + } +#endif + +#ifdef SUNSHINE_BUILD_DRM + std::vector + kms_display_names(); + std::shared_ptr + kms_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); + + bool + verify_kms() { + return !kms_display_names().empty(); + } +#endif + +#ifdef SUNSHINE_BUILD_X11 + std::vector + x11_display_names(); + std::shared_ptr + x11_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); + + bool + verify_x11() { + return window_system == window_system_e::X11 && !x11_display_names().empty(); + } +#endif + + std::vector + display_names(mem_type_e hwdevice_type) { +#ifdef SUNSHINE_BUILD_CUDA + // display using NvFBC only supports mem_type_e::cuda + if (sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) return nvfbc_display_names(); +#endif +#ifdef SUNSHINE_BUILD_WAYLAND + if (sources[source::WAYLAND]) return wl_display_names(); +#endif +#ifdef SUNSHINE_BUILD_DRM + if (sources[source::KMS]) return kms_display_names(); +#endif +#ifdef SUNSHINE_BUILD_X11 + if (sources[source::X11]) return x11_display_names(); +#endif + return {}; + } + + std::shared_ptr + display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { +#ifdef SUNSHINE_BUILD_CUDA + if (sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) { + BOOST_LOG(info) << "Screencasting with NvFBC"sv; + return nvfbc_display(hwdevice_type, display_name, config); } - - // Call sendmmsg() until all messages are sent - size_t blocks_sent = 0; - while(blocks_sent < send_info.block_count) { - int msgs_sent = sendmmsg(sockfd, &msgs[blocks_sent], send_info.block_count - blocks_sent, 0); - if(msgs_sent < 0) { - // If there's no send buffer space, wait for some to be available - if(errno == EAGAIN) { - struct pollfd pfd; - - pfd.fd = sockfd; - pfd.events = POLLOUT; - - if(poll(&pfd, 1, -1) != 1) { - BOOST_LOG(warning) << "poll() failed: "sv << errno; - break; - } - - // Try to send again - continue; - } - - BOOST_LOG(warning) << "sendmmsg() failed: "sv << errno; - return false; - } - - blocks_sent += msgs_sent; +#endif +#ifdef SUNSHINE_BUILD_WAYLAND + if (sources[source::WAYLAND]) { + BOOST_LOG(info) << "Screencasting with Wayland's protocol"sv; + return wl_display(hwdevice_type, display_name, config); } - - return true; - } -} - -class qos_t : public deinit_t { -public: - qos_t(int sockfd, int level, int option) : sockfd(sockfd), level(level), option(option) {} - - virtual ~qos_t() { - int reset_val = -1; - if(setsockopt(sockfd, level, option, &reset_val, sizeof(reset_val)) < 0) { - BOOST_LOG(warning) << "Failed to reset IP TOS: "sv << errno; +#endif +#ifdef SUNSHINE_BUILD_DRM + if (sources[source::KMS]) { + BOOST_LOG(info) << "Screencasting with KMS"sv; + return kms_display(hwdevice_type, display_name, config); } - } +#endif +#ifdef SUNSHINE_BUILD_X11 + if (sources[source::X11]) { + BOOST_LOG(info) << "Screencasting with X11"sv; + return x11_display(hwdevice_type, display_name, config); + } +#endif -private: - int sockfd; - int level; - int option; -}; - -std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { - int sockfd = (int)native_socket; - - int level; - int option; - if(address.is_v6()) { - level = SOL_IPV6; - option = IPV6_TCLASS; - } - else { - level = SOL_IP; - option = IP_TOS; - } - - // The specific DSCP values here are chosen to be consistent with Windows - int dscp; - switch(data_type) { - case qos_data_type_e::video: - dscp = 40; - break; - case qos_data_type_e::audio: - dscp = 56; - break; - default: - BOOST_LOG(error) << "Unknown traffic type: "sv << (int)data_type; return nullptr; } - // Shift to put the DSCP value in the correct position in the TOS field - dscp <<= 2; + std::unique_ptr + init() { + // These are allowed to fail. + gbm::init(); + va::init(); - if(setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) < 0) { - return nullptr; - } - - return std::make_unique(sockfd, level, option); -} - -namespace source { -enum source_e : std::size_t { -#ifdef SUNSHINE_BUILD_CUDA - NVFBC, -#endif + window_system = window_system_e::NONE; #ifdef SUNSHINE_BUILD_WAYLAND - WAYLAND, -#endif -#ifdef SUNSHINE_BUILD_DRM - KMS, -#endif -#ifdef SUNSHINE_BUILD_X11 - X11, -#endif - MAX_FLAGS -}; -} // namespace source - -static std::bitset sources; - -#ifdef SUNSHINE_BUILD_CUDA -std::vector nvfbc_display_names(); -std::shared_ptr nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); - -bool verify_nvfbc() { - return !nvfbc_display_names().empty(); -} -#endif - -#ifdef SUNSHINE_BUILD_WAYLAND -std::vector wl_display_names(); -std::shared_ptr wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); - -bool verify_wl() { - return window_system == window_system_e::WAYLAND && !wl_display_names().empty(); -} -#endif - -#ifdef SUNSHINE_BUILD_DRM -std::vector kms_display_names(); -std::shared_ptr kms_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); - -bool verify_kms() { - return !kms_display_names().empty(); -} -#endif - -#ifdef SUNSHINE_BUILD_X11 -std::vector x11_display_names(); -std::shared_ptr x11_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config); - -bool verify_x11() { - return window_system == window_system_e::X11 && !x11_display_names().empty(); -} -#endif - -std::vector display_names(mem_type_e hwdevice_type) { -#ifdef SUNSHINE_BUILD_CUDA - // display using NvFBC only supports mem_type_e::cuda - if(sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) return nvfbc_display_names(); -#endif -#ifdef SUNSHINE_BUILD_WAYLAND - if(sources[source::WAYLAND]) return wl_display_names(); -#endif -#ifdef SUNSHINE_BUILD_DRM - if(sources[source::KMS]) return kms_display_names(); -#endif -#ifdef SUNSHINE_BUILD_X11 - if(sources[source::X11]) return x11_display_names(); -#endif - return {}; -} - -std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { -#ifdef SUNSHINE_BUILD_CUDA - if(sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) { - BOOST_LOG(info) << "Screencasting with NvFBC"sv; - return nvfbc_display(hwdevice_type, display_name, config); - } -#endif -#ifdef SUNSHINE_BUILD_WAYLAND - if(sources[source::WAYLAND]) { - BOOST_LOG(info) << "Screencasting with Wayland's protocol"sv; - return wl_display(hwdevice_type, display_name, config); - } -#endif -#ifdef SUNSHINE_BUILD_DRM - if(sources[source::KMS]) { - BOOST_LOG(info) << "Screencasting with KMS"sv; - return kms_display(hwdevice_type, display_name, config); - } -#endif -#ifdef SUNSHINE_BUILD_X11 - if(sources[source::X11]) { - BOOST_LOG(info) << "Screencasting with X11"sv; - return x11_display(hwdevice_type, display_name, config); - } -#endif - - return nullptr; -} - -std::unique_ptr init() { - // These are allowed to fail. - gbm::init(); - va::init(); - - window_system = window_system_e::NONE; -#ifdef SUNSHINE_BUILD_WAYLAND - if(std::getenv("WAYLAND_DISPLAY")) { - window_system = window_system_e::WAYLAND; - } + if (std::getenv("WAYLAND_DISPLAY")) { + window_system = window_system_e::WAYLAND; + } #endif #if defined(SUNSHINE_BUILD_X11) || defined(SUNSHINE_BUILD_CUDA) - if(std::getenv("DISPLAY") && window_system != window_system_e::WAYLAND) { - if(std::getenv("WAYLAND_DISPLAY")) { - BOOST_LOG(warning) << "Wayland detected, yet sunshine will use X11 for screencasting, screencasting will only work on XWayland applications"sv; - } + if (std::getenv("DISPLAY") && window_system != window_system_e::WAYLAND) { + if (std::getenv("WAYLAND_DISPLAY")) { + BOOST_LOG(warning) << "Wayland detected, yet sunshine will use X11 for screencasting, screencasting will only work on XWayland applications"sv; + } - window_system = window_system_e::X11; - } + window_system = window_system_e::X11; + } #endif #ifdef SUNSHINE_BUILD_CUDA - if(config::video.capture.empty() || config::video.capture == "nvfbc") { - if(verify_nvfbc()) { - sources[source::NVFBC] = true; - } - } -#endif -#ifdef SUNSHINE_BUILD_WAYLAND - if(config::video.capture.empty() || config::video.capture == "wlr") { - if(verify_wl()) { - sources[source::WAYLAND] = true; - } - } -#endif -#ifdef SUNSHINE_BUILD_DRM - if(config::video.capture.empty() || config::video.capture == "kms") { - if(verify_kms()) { - if(window_system == window_system_e::WAYLAND) { - // On Wayland, using KMS, the cursor is unreliable. - // Hide it by default - display_cursor = false; + if (config::video.capture.empty() || config::video.capture == "nvfbc") { + if (verify_nvfbc()) { + sources[source::NVFBC] = true; } } +#endif +#ifdef SUNSHINE_BUILD_WAYLAND + if (config::video.capture.empty() || config::video.capture == "wlr") { + if (verify_wl()) { + sources[source::WAYLAND] = true; + } + } +#endif +#ifdef SUNSHINE_BUILD_DRM + if (config::video.capture.empty() || config::video.capture == "kms") { + if (verify_kms()) { + if (window_system == window_system_e::WAYLAND) { + // On Wayland, using KMS, the cursor is unreliable. + // Hide it by default + display_cursor = false; + } + } - sources[source::KMS] = true; - } + sources[source::KMS] = true; + } #endif #ifdef SUNSHINE_BUILD_X11 - if(config::video.capture.empty() || config::video.capture == "x11") { - if(verify_x11()) { - sources[source::X11] = true; + if (config::video.capture.empty() || config::video.capture == "x11") { + if (verify_x11()) { + sources[source::X11] = true; + } } - } #endif - if(sources.none()) { - BOOST_LOG(error) << "Unable to initialize capture method"sv; - return nullptr; - } + if (sources.none()) { + BOOST_LOG(error) << "Unable to initialize capture method"sv; + return nullptr; + } - if(!gladLoaderLoadEGL(EGL_NO_DISPLAY) || !eglGetPlatformDisplay) { - BOOST_LOG(warning) << "Couldn't load EGL library"sv; - } + if (!gladLoaderLoadEGL(EGL_NO_DISPLAY) || !eglGetPlatformDisplay) { + BOOST_LOG(warning) << "Couldn't load EGL library"sv; + } - return std::make_unique(); -} -} // namespace platf + return std::make_unique(); + } +} // namespace platf diff --git a/src/platform/linux/misc.h b/src/platform/linux/misc.h index 515087d7..e4a59f67 100644 --- a/src/platform/linux/misc.h +++ b/src/platform/linux/misc.h @@ -7,7 +7,7 @@ #include "src/utility.h" KITTY_USING_MOVE_T(file_t, int, -1, { - if(el >= 0) { + if (el >= 0) { close(el); } }); @@ -21,11 +21,13 @@ enum class window_system_e { extern window_system_e window_system; namespace dyn { -typedef void (*apiproc)(void); + typedef void (*apiproc)(void); -int load(void *handle, const std::vector> &funcs, bool strict = true); -void *handle(const std::vector &libs); + int + load(void *handle, const std::vector> &funcs, bool strict = true); + void * + handle(const std::vector &libs); -} // namespace dyn +} // namespace dyn #endif \ No newline at end of file diff --git a/src/platform/linux/publish.cpp b/src/platform/linux/publish.cpp index 19cbb3f5..6c6d7ede 100644 --- a/src/platform/linux/publish.cpp +++ b/src/platform/linux/publish.cpp @@ -12,418 +12,426 @@ using namespace std::literals; namespace avahi { -/** Error codes used by avahi */ -enum err_e { - OK = 0, /**< OK */ - ERR_FAILURE = -1, /**< Generic error code */ - ERR_BAD_STATE = -2, /**< Object was in a bad state */ - ERR_INVALID_HOST_NAME = -3, /**< Invalid host name */ - ERR_INVALID_DOMAIN_NAME = -4, /**< Invalid domain name */ - ERR_NO_NETWORK = -5, /**< No suitable network protocol available */ - ERR_INVALID_TTL = -6, /**< Invalid DNS TTL */ - ERR_IS_PATTERN = -7, /**< RR key is pattern */ - ERR_COLLISION = -8, /**< Name collision */ - ERR_INVALID_RECORD = -9, /**< Invalid RR */ + /** Error codes used by avahi */ + enum err_e { + OK = 0, /**< OK */ + ERR_FAILURE = -1, /**< Generic error code */ + ERR_BAD_STATE = -2, /**< Object was in a bad state */ + ERR_INVALID_HOST_NAME = -3, /**< Invalid host name */ + ERR_INVALID_DOMAIN_NAME = -4, /**< Invalid domain name */ + ERR_NO_NETWORK = -5, /**< No suitable network protocol available */ + ERR_INVALID_TTL = -6, /**< Invalid DNS TTL */ + ERR_IS_PATTERN = -7, /**< RR key is pattern */ + ERR_COLLISION = -8, /**< Name collision */ + ERR_INVALID_RECORD = -9, /**< Invalid RR */ - ERR_INVALID_SERVICE_NAME = -10, /**< Invalid service name */ - ERR_INVALID_SERVICE_TYPE = -11, /**< Invalid service type */ - ERR_INVALID_PORT = -12, /**< Invalid port number */ - ERR_INVALID_KEY = -13, /**< Invalid key */ - ERR_INVALID_ADDRESS = -14, /**< Invalid address */ - ERR_TIMEOUT = -15, /**< Timeout reached */ - ERR_TOO_MANY_CLIENTS = -16, /**< Too many clients */ - ERR_TOO_MANY_OBJECTS = -17, /**< Too many objects */ - ERR_TOO_MANY_ENTRIES = -18, /**< Too many entries */ - ERR_OS = -19, /**< OS error */ + ERR_INVALID_SERVICE_NAME = -10, /**< Invalid service name */ + ERR_INVALID_SERVICE_TYPE = -11, /**< Invalid service type */ + ERR_INVALID_PORT = -12, /**< Invalid port number */ + ERR_INVALID_KEY = -13, /**< Invalid key */ + ERR_INVALID_ADDRESS = -14, /**< Invalid address */ + ERR_TIMEOUT = -15, /**< Timeout reached */ + ERR_TOO_MANY_CLIENTS = -16, /**< Too many clients */ + ERR_TOO_MANY_OBJECTS = -17, /**< Too many objects */ + ERR_TOO_MANY_ENTRIES = -18, /**< Too many entries */ + ERR_OS = -19, /**< OS error */ - ERR_ACCESS_DENIED = -20, /**< Access denied */ - ERR_INVALID_OPERATION = -21, /**< Invalid operation */ - ERR_DBUS_ERROR = -22, /**< An unexpected D-Bus error occurred */ - ERR_DISCONNECTED = -23, /**< Daemon connection failed */ - ERR_NO_MEMORY = -24, /**< Memory exhausted */ - ERR_INVALID_OBJECT = -25, /**< The object passed to this function was invalid */ - ERR_NO_DAEMON = -26, /**< Daemon not running */ - ERR_INVALID_INTERFACE = -27, /**< Invalid interface */ - ERR_INVALID_PROTOCOL = -28, /**< Invalid protocol */ - ERR_INVALID_FLAGS = -29, /**< Invalid flags */ + ERR_ACCESS_DENIED = -20, /**< Access denied */ + ERR_INVALID_OPERATION = -21, /**< Invalid operation */ + ERR_DBUS_ERROR = -22, /**< An unexpected D-Bus error occurred */ + ERR_DISCONNECTED = -23, /**< Daemon connection failed */ + ERR_NO_MEMORY = -24, /**< Memory exhausted */ + ERR_INVALID_OBJECT = -25, /**< The object passed to this function was invalid */ + ERR_NO_DAEMON = -26, /**< Daemon not running */ + ERR_INVALID_INTERFACE = -27, /**< Invalid interface */ + ERR_INVALID_PROTOCOL = -28, /**< Invalid protocol */ + ERR_INVALID_FLAGS = -29, /**< Invalid flags */ - ERR_NOT_FOUND = -30, /**< Not found */ - ERR_INVALID_CONFIG = -31, /**< Configuration error */ - ERR_VERSION_MISMATCH = -32, /**< Verson mismatch */ - ERR_INVALID_SERVICE_SUBTYPE = -33, /**< Invalid service subtype */ - ERR_INVALID_PACKET = -34, /**< Invalid packet */ - ERR_INVALID_DNS_ERROR = -35, /**< Invlaid DNS return code */ - ERR_DNS_FORMERR = -36, /**< DNS Error: Form error */ - ERR_DNS_SERVFAIL = -37, /**< DNS Error: Server Failure */ - ERR_DNS_NXDOMAIN = -38, /**< DNS Error: No such domain */ - ERR_DNS_NOTIMP = -39, /**< DNS Error: Not implemented */ + ERR_NOT_FOUND = -30, /**< Not found */ + ERR_INVALID_CONFIG = -31, /**< Configuration error */ + ERR_VERSION_MISMATCH = -32, /**< Verson mismatch */ + ERR_INVALID_SERVICE_SUBTYPE = -33, /**< Invalid service subtype */ + ERR_INVALID_PACKET = -34, /**< Invalid packet */ + ERR_INVALID_DNS_ERROR = -35, /**< Invlaid DNS return code */ + ERR_DNS_FORMERR = -36, /**< DNS Error: Form error */ + ERR_DNS_SERVFAIL = -37, /**< DNS Error: Server Failure */ + ERR_DNS_NXDOMAIN = -38, /**< DNS Error: No such domain */ + ERR_DNS_NOTIMP = -39, /**< DNS Error: Not implemented */ - ERR_DNS_REFUSED = -40, /**< DNS Error: Operation refused */ - ERR_DNS_YXDOMAIN = -41, - ERR_DNS_YXRRSET = -42, - ERR_DNS_NXRRSET = -43, - ERR_DNS_NOTAUTH = -44, /**< DNS Error: Not authorized */ - ERR_DNS_NOTZONE = -45, - ERR_INVALID_RDATA = -46, /**< Invalid RDATA */ - ERR_INVALID_DNS_CLASS = -47, /**< Invalid DNS class */ - ERR_INVALID_DNS_TYPE = -48, /**< Invalid DNS type */ - ERR_NOT_SUPPORTED = -49, /**< Not supported */ + ERR_DNS_REFUSED = -40, /**< DNS Error: Operation refused */ + ERR_DNS_YXDOMAIN = -41, + ERR_DNS_YXRRSET = -42, + ERR_DNS_NXRRSET = -43, + ERR_DNS_NOTAUTH = -44, /**< DNS Error: Not authorized */ + ERR_DNS_NOTZONE = -45, + ERR_INVALID_RDATA = -46, /**< Invalid RDATA */ + ERR_INVALID_DNS_CLASS = -47, /**< Invalid DNS class */ + ERR_INVALID_DNS_TYPE = -48, /**< Invalid DNS type */ + ERR_NOT_SUPPORTED = -49, /**< Not supported */ - ERR_NOT_PERMITTED = -50, /**< Operation not permitted */ - ERR_INVALID_ARGUMENT = -51, /**< Invalid argument */ - ERR_IS_EMPTY = -52, /**< Is empty */ - ERR_NO_CHANGE = -53, /**< The requested operation is invalid because it is redundant */ + ERR_NOT_PERMITTED = -50, /**< Operation not permitted */ + ERR_INVALID_ARGUMENT = -51, /**< Invalid argument */ + ERR_IS_EMPTY = -52, /**< Is empty */ + ERR_NO_CHANGE = -53, /**< The requested operation is invalid because it is redundant */ - ERR_MAX = -54 -}; - -constexpr auto IF_UNSPEC = -1; -enum proto { - PROTO_INET = 0, /**< IPv4 */ - PROTO_INET6 = 1, /**< IPv6 */ - PROTO_UNSPEC = -1 /**< Unspecified/all protocol(s) */ -}; - -enum ServerState { - SERVER_INVALID, /**< Invalid state (initial) */ - SERVER_REGISTERING, /**< Host RRs are being registered */ - SERVER_RUNNING, /**< All host RRs have been established */ - SERVER_COLLISION, /**< There is a collision with a host RR. All host RRs have been withdrawn, the user should set a new host name via avahi_server_set_host_name() */ - SERVER_FAILURE /**< Some fatal failure happened, the server is unable to proceed */ -}; - -enum ClientState { - CLIENT_S_REGISTERING = SERVER_REGISTERING, /**< Server state: REGISTERING */ - CLIENT_S_RUNNING = SERVER_RUNNING, /**< Server state: RUNNING */ - CLIENT_S_COLLISION = SERVER_COLLISION, /**< Server state: COLLISION */ - CLIENT_FAILURE = 100, /**< Some kind of error happened on the client side */ - CLIENT_CONNECTING = 101 /**< We're still connecting. This state is only entered when AVAHI_CLIENT_NO_FAIL has been passed to avahi_client_new() and the daemon is not yet available. */ -}; - -enum EntryGroupState { - ENTRY_GROUP_UNCOMMITED, /**< The group has not yet been commited, the user must still call avahi_entry_group_commit() */ - ENTRY_GROUP_REGISTERING, /**< The entries of the group are currently being registered */ - ENTRY_GROUP_ESTABLISHED, /**< The entries have successfully been established */ - ENTRY_GROUP_COLLISION, /**< A name collision for one of the entries in the group has been detected, the entries have been withdrawn */ - ENTRY_GROUP_FAILURE /**< Some kind of failure happened, the entries have been withdrawn */ -}; - -enum ClientFlags { - CLIENT_IGNORE_USER_CONFIG = 1, /**< Don't read user configuration */ - CLIENT_NO_FAIL = 2 /**< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear */ -}; - -/** Some flags for publishing functions */ -enum PublishFlags { - PUBLISH_UNIQUE = 1, /**< For raw records: The RRset is intended to be unique */ - PUBLISH_NO_PROBE = 2, /**< For raw records: Though the RRset is intended to be unique no probes shall be sent */ - PUBLISH_NO_ANNOUNCE = 4, /**< For raw records: Do not announce this RR to other hosts */ - PUBLISH_ALLOW_MULTIPLE = 8, /**< For raw records: Allow multiple local records of this type, even if they are intended to be unique */ - /** \cond fulldocs */ - PUBLISH_NO_REVERSE = 16, /**< For address records: don't create a reverse (PTR) entry */ - PUBLISH_NO_COOKIE = 32, /**< For service records: do not implicitly add the local service cookie to TXT data */ - /** \endcond */ - PUBLISH_UPDATE = 64, /**< Update existing records instead of adding new ones */ - /** \cond fulldocs */ - PUBLISH_USE_WIDE_AREA = 128, /**< Register the record using wide area DNS (i.e. unicast DNS update) */ - PUBLISH_USE_MULTICAST = 256 /**< Register the record using multicast DNS */ - /** \endcond */ -}; - -using IfIndex = int; -using Protocol = int; - -struct EntryGroup; -struct Poll; -struct SimplePoll; -struct Client; - -typedef void (*ClientCallback)(Client *, ClientState, void *userdata); -typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata); - -typedef void (*free_fn)(void *); - -typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error); -typedef void (*client_free_fn)(Client *); -typedef char *(*alternative_service_name_fn)(char *); - -typedef Client *(*entry_group_get_client_fn)(EntryGroup *); - -typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata); -typedef int (*entry_group_add_service_fn)( - EntryGroup *group, - IfIndex interface, - Protocol protocol, - PublishFlags flags, - const char *name, - const char *type, - const char *domain, - const char *host, - uint16_t port, - ...); - -typedef int (*entry_group_is_empty_fn)(EntryGroup *); -typedef int (*entry_group_reset_fn)(EntryGroup *); -typedef int (*entry_group_commit_fn)(EntryGroup *); - -typedef char *(*strdup_fn)(const char *); -typedef char *(*strerror_fn)(int); -typedef int (*client_errno_fn)(Client *); - -typedef Poll *(*simple_poll_get_fn)(SimplePoll *); -typedef int (*simple_poll_loop_fn)(SimplePoll *); -typedef void (*simple_poll_quit_fn)(SimplePoll *); -typedef SimplePoll *(*simple_poll_new_fn)(); -typedef void (*simple_poll_free_fn)(SimplePoll *); - -free_fn free; -client_new_fn client_new; -client_free_fn client_free; -alternative_service_name_fn alternative_service_name; -entry_group_get_client_fn entry_group_get_client; -entry_group_new_fn entry_group_new; -entry_group_add_service_fn entry_group_add_service; -entry_group_is_empty_fn entry_group_is_empty; -entry_group_reset_fn entry_group_reset; -entry_group_commit_fn entry_group_commit; -strdup_fn strdup; -strerror_fn strerror; -client_errno_fn client_errno; -simple_poll_get_fn simple_poll_get; -simple_poll_loop_fn simple_poll_loop; -simple_poll_quit_fn simple_poll_quit; -simple_poll_new_fn simple_poll_new; -simple_poll_free_fn simple_poll_free; - - -int init_common() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if(funcs_loaded) return 0; - - if(!handle) { - handle = dyn::handle({ "libavahi-common.so.3", "libavahi-common.so" }); - if(!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *)&alternative_service_name, "avahi_alternative_service_name" }, - { (dyn::apiproc *)&free, "avahi_free" }, - { (dyn::apiproc *)&strdup, "avahi_strdup" }, - { (dyn::apiproc *)&strerror, "avahi_strerror" }, - { (dyn::apiproc *)&simple_poll_get, "avahi_simple_poll_get" }, - { (dyn::apiproc *)&simple_poll_loop, "avahi_simple_poll_loop" }, - { (dyn::apiproc *)&simple_poll_quit, "avahi_simple_poll_quit" }, - { (dyn::apiproc *)&simple_poll_new, "avahi_simple_poll_new" }, - { (dyn::apiproc *)&simple_poll_free, "avahi_simple_poll_free" }, + ERR_MAX = -54 }; - if(dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; -} - -int init_client() { - if(init_common()) { - return -1; - } - - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if(funcs_loaded) return 0; - - if(!handle) { - handle = dyn::handle({ "libavahi-client.so.3", "libavahi-client.so" }); - if(!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *)&client_new, "avahi_client_new" }, - { (dyn::apiproc *)&client_free, "avahi_client_free" }, - { (dyn::apiproc *)&entry_group_get_client, "avahi_entry_group_get_client" }, - { (dyn::apiproc *)&entry_group_new, "avahi_entry_group_new" }, - { (dyn::apiproc *)&entry_group_add_service, "avahi_entry_group_add_service" }, - { (dyn::apiproc *)&entry_group_is_empty, "avahi_entry_group_is_empty" }, - { (dyn::apiproc *)&entry_group_reset, "avahi_entry_group_reset" }, - { (dyn::apiproc *)&entry_group_commit, "avahi_entry_group_commit" }, - { (dyn::apiproc *)&client_errno, "avahi_client_errno" }, + constexpr auto IF_UNSPEC = -1; + enum proto { + PROTO_INET = 0, /**< IPv4 */ + PROTO_INET6 = 1, /**< IPv6 */ + PROTO_UNSPEC = -1 /**< Unspecified/all protocol(s) */ }; - if(dyn::load(handle, funcs)) { - return -1; + enum ServerState { + SERVER_INVALID, /**< Invalid state (initial) */ + SERVER_REGISTERING, /**< Host RRs are being registered */ + SERVER_RUNNING, /**< All host RRs have been established */ + SERVER_COLLISION, /**< There is a collision with a host RR. All host RRs have been withdrawn, the user should set a new host name via avahi_server_set_host_name() */ + SERVER_FAILURE /**< Some fatal failure happened, the server is unable to proceed */ + }; + + enum ClientState { + CLIENT_S_REGISTERING = SERVER_REGISTERING, /**< Server state: REGISTERING */ + CLIENT_S_RUNNING = SERVER_RUNNING, /**< Server state: RUNNING */ + CLIENT_S_COLLISION = SERVER_COLLISION, /**< Server state: COLLISION */ + CLIENT_FAILURE = 100, /**< Some kind of error happened on the client side */ + CLIENT_CONNECTING = 101 /**< We're still connecting. This state is only entered when AVAHI_CLIENT_NO_FAIL has been passed to avahi_client_new() and the daemon is not yet available. */ + }; + + enum EntryGroupState { + ENTRY_GROUP_UNCOMMITED, /**< The group has not yet been commited, the user must still call avahi_entry_group_commit() */ + ENTRY_GROUP_REGISTERING, /**< The entries of the group are currently being registered */ + ENTRY_GROUP_ESTABLISHED, /**< The entries have successfully been established */ + ENTRY_GROUP_COLLISION, /**< A name collision for one of the entries in the group has been detected, the entries have been withdrawn */ + ENTRY_GROUP_FAILURE /**< Some kind of failure happened, the entries have been withdrawn */ + }; + + enum ClientFlags { + CLIENT_IGNORE_USER_CONFIG = 1, /**< Don't read user configuration */ + CLIENT_NO_FAIL = 2 /**< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear */ + }; + + /** Some flags for publishing functions */ + enum PublishFlags { + PUBLISH_UNIQUE = 1, /**< For raw records: The RRset is intended to be unique */ + PUBLISH_NO_PROBE = 2, /**< For raw records: Though the RRset is intended to be unique no probes shall be sent */ + PUBLISH_NO_ANNOUNCE = 4, /**< For raw records: Do not announce this RR to other hosts */ + PUBLISH_ALLOW_MULTIPLE = 8, /**< For raw records: Allow multiple local records of this type, even if they are intended to be unique */ + /** \cond fulldocs */ + PUBLISH_NO_REVERSE = 16, /**< For address records: don't create a reverse (PTR) entry */ + PUBLISH_NO_COOKIE = 32, /**< For service records: do not implicitly add the local service cookie to TXT data */ + /** \endcond */ + PUBLISH_UPDATE = 64, /**< Update existing records instead of adding new ones */ + /** \cond fulldocs */ + PUBLISH_USE_WIDE_AREA = 128, /**< Register the record using wide area DNS (i.e. unicast DNS update) */ + PUBLISH_USE_MULTICAST = 256 /**< Register the record using multicast DNS */ + /** \endcond */ + }; + + using IfIndex = int; + using Protocol = int; + + struct EntryGroup; + struct Poll; + struct SimplePoll; + struct Client; + + typedef void (*ClientCallback)(Client *, ClientState, void *userdata); + typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata); + + typedef void (*free_fn)(void *); + + typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error); + typedef void (*client_free_fn)(Client *); + typedef char *(*alternative_service_name_fn)(char *); + + typedef Client *(*entry_group_get_client_fn)(EntryGroup *); + + typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata); + typedef int (*entry_group_add_service_fn)( + EntryGroup *group, + IfIndex interface, + Protocol protocol, + PublishFlags flags, + const char *name, + const char *type, + const char *domain, + const char *host, + uint16_t port, + ...); + + typedef int (*entry_group_is_empty_fn)(EntryGroup *); + typedef int (*entry_group_reset_fn)(EntryGroup *); + typedef int (*entry_group_commit_fn)(EntryGroup *); + + typedef char *(*strdup_fn)(const char *); + typedef char *(*strerror_fn)(int); + typedef int (*client_errno_fn)(Client *); + + typedef Poll *(*simple_poll_get_fn)(SimplePoll *); + typedef int (*simple_poll_loop_fn)(SimplePoll *); + typedef void (*simple_poll_quit_fn)(SimplePoll *); + typedef SimplePoll *(*simple_poll_new_fn)(); + typedef void (*simple_poll_free_fn)(SimplePoll *); + + free_fn free; + client_new_fn client_new; + client_free_fn client_free; + alternative_service_name_fn alternative_service_name; + entry_group_get_client_fn entry_group_get_client; + entry_group_new_fn entry_group_new; + entry_group_add_service_fn entry_group_add_service; + entry_group_is_empty_fn entry_group_is_empty; + entry_group_reset_fn entry_group_reset; + entry_group_commit_fn entry_group_commit; + strdup_fn strdup; + strerror_fn strerror; + client_errno_fn client_errno; + simple_poll_get_fn simple_poll_get; + simple_poll_loop_fn simple_poll_loop; + simple_poll_quit_fn simple_poll_quit; + simple_poll_new_fn simple_poll_new; + simple_poll_free_fn simple_poll_free; + + int + init_common() { + static void *handle { nullptr }; + static bool funcs_loaded = false; + + if (funcs_loaded) return 0; + + if (!handle) { + handle = dyn::handle({ "libavahi-common.so.3", "libavahi-common.so" }); + if (!handle) { + return -1; + } + } + + std::vector> funcs { + { (dyn::apiproc *) &alternative_service_name, "avahi_alternative_service_name" }, + { (dyn::apiproc *) &free, "avahi_free" }, + { (dyn::apiproc *) &strdup, "avahi_strdup" }, + { (dyn::apiproc *) &strerror, "avahi_strerror" }, + { (dyn::apiproc *) &simple_poll_get, "avahi_simple_poll_get" }, + { (dyn::apiproc *) &simple_poll_loop, "avahi_simple_poll_loop" }, + { (dyn::apiproc *) &simple_poll_quit, "avahi_simple_poll_quit" }, + { (dyn::apiproc *) &simple_poll_new, "avahi_simple_poll_new" }, + { (dyn::apiproc *) &simple_poll_free, "avahi_simple_poll_free" }, + }; + + if (dyn::load(handle, funcs)) { + return -1; + } + + funcs_loaded = true; + return 0; } - funcs_loaded = true; - return 0; -} -} // namespace avahi + int + init_client() { + if (init_common()) { + return -1; + } + + static void *handle { nullptr }; + static bool funcs_loaded = false; + + if (funcs_loaded) return 0; + + if (!handle) { + handle = dyn::handle({ "libavahi-client.so.3", "libavahi-client.so" }); + if (!handle) { + return -1; + } + } + + std::vector> funcs { + { (dyn::apiproc *) &client_new, "avahi_client_new" }, + { (dyn::apiproc *) &client_free, "avahi_client_free" }, + { (dyn::apiproc *) &entry_group_get_client, "avahi_entry_group_get_client" }, + { (dyn::apiproc *) &entry_group_new, "avahi_entry_group_new" }, + { (dyn::apiproc *) &entry_group_add_service, "avahi_entry_group_add_service" }, + { (dyn::apiproc *) &entry_group_is_empty, "avahi_entry_group_is_empty" }, + { (dyn::apiproc *) &entry_group_reset, "avahi_entry_group_reset" }, + { (dyn::apiproc *) &entry_group_commit, "avahi_entry_group_commit" }, + { (dyn::apiproc *) &client_errno, "avahi_client_errno" }, + }; + + if (dyn::load(handle, funcs)) { + return -1; + } + + funcs_loaded = true; + return 0; + } +} // namespace avahi namespace platf::publish { -template -void free(T *p) { - avahi::free(p); -} - -template -using ptr_t = util::safe_ptr>; -using client_t = util::dyn_safe_ptr; -using poll_t = util::dyn_safe_ptr; - -avahi::EntryGroup *group = nullptr; - -poll_t poll; -client_t client; - -ptr_t name; - -void create_services(avahi::Client *c); - -void entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) { - group = g; - - switch(state) { - case avahi::ENTRY_GROUP_ESTABLISHED: - BOOST_LOG(info) << "Avahi service " << name.get() << " successfully established."; - break; - case avahi::ENTRY_GROUP_COLLISION: - name.reset(avahi::alternative_service_name(name.get())); - - BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get(); - - create_services(avahi::entry_group_get_client(g)); - break; - case avahi::ENTRY_GROUP_FAILURE: - BOOST_LOG(error) << "Avahi entry group failure: " << avahi::strerror(avahi::client_errno(avahi::entry_group_get_client(g))); - avahi::simple_poll_quit(poll.get()); - break; - case avahi::ENTRY_GROUP_UNCOMMITED: - case avahi::ENTRY_GROUP_REGISTERING:; + template + void + free(T *p) { + avahi::free(p); } -} -void create_services(avahi::Client *c) { - int ret; + template + using ptr_t = util::safe_ptr>; + using client_t = util::dyn_safe_ptr; + using poll_t = util::dyn_safe_ptr; - auto fg = util::fail_guard([]() { - avahi::simple_poll_quit(poll.get()); - }); + avahi::EntryGroup *group = nullptr; - if(!group) { - if(!(group = avahi::entry_group_new(c, entry_group_callback, nullptr))) { - BOOST_LOG(error) << "avahi::entry_group_new() failed: "sv << avahi::strerror(avahi::client_errno(c)); - return; + poll_t poll; + client_t client; + + ptr_t name; + + void + create_services(avahi::Client *c); + + void + entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) { + group = g; + + switch (state) { + case avahi::ENTRY_GROUP_ESTABLISHED: + BOOST_LOG(info) << "Avahi service " << name.get() << " successfully established."; + break; + case avahi::ENTRY_GROUP_COLLISION: + name.reset(avahi::alternative_service_name(name.get())); + + BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get(); + + create_services(avahi::entry_group_get_client(g)); + break; + case avahi::ENTRY_GROUP_FAILURE: + BOOST_LOG(error) << "Avahi entry group failure: " << avahi::strerror(avahi::client_errno(avahi::entry_group_get_client(g))); + avahi::simple_poll_quit(poll.get()); + break; + case avahi::ENTRY_GROUP_UNCOMMITED: + case avahi::ENTRY_GROUP_REGISTERING:; } } - if(avahi::entry_group_is_empty(group)) { - BOOST_LOG(info) << "Adding avahi service "sv << name.get(); + void + create_services(avahi::Client *c) { + int ret; - ret = avahi::entry_group_add_service( - group, - avahi::IF_UNSPEC, avahi::PROTO_UNSPEC, - avahi::PublishFlags(0), - name.get(), - SERVICE_TYPE, - nullptr, nullptr, - map_port(nvhttp::PORT_HTTP), - nullptr); + auto fg = util::fail_guard([]() { + avahi::simple_poll_quit(poll.get()); + }); - if(ret < 0) { - if(ret == avahi::ERR_COLLISION) { - // A service name collision with a local service happened. Let's pick a new name - name.reset(avahi::alternative_service_name(name.get())); - BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get(); + if (!group) { + if (!(group = avahi::entry_group_new(c, entry_group_callback, nullptr))) { + BOOST_LOG(error) << "avahi::entry_group_new() failed: "sv << avahi::strerror(avahi::client_errno(c)); + return; + } + } - avahi::entry_group_reset(group); + if (avahi::entry_group_is_empty(group)) { + BOOST_LOG(info) << "Adding avahi service "sv << name.get(); - create_services(c); + ret = avahi::entry_group_add_service( + group, + avahi::IF_UNSPEC, avahi::PROTO_UNSPEC, + avahi::PublishFlags(0), + name.get(), + SERVICE_TYPE, + nullptr, nullptr, + map_port(nvhttp::PORT_HTTP), + nullptr); - fg.disable(); + if (ret < 0) { + if (ret == avahi::ERR_COLLISION) { + // A service name collision with a local service happened. Let's pick a new name + name.reset(avahi::alternative_service_name(name.get())); + BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get(); + + avahi::entry_group_reset(group); + + create_services(c); + + fg.disable(); + return; + } + + BOOST_LOG(error) << "Failed to add "sv << SERVICE_TYPE << " service: "sv << avahi::strerror(ret); return; } - BOOST_LOG(error) << "Failed to add "sv << SERVICE_TYPE << " service: "sv << avahi::strerror(ret); - return; + ret = avahi::entry_group_commit(group); + if (ret < 0) { + BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret); + return; + } } - ret = avahi::entry_group_commit(group); - if(ret < 0) { - BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret); - return; + fg.disable(); + } + + void + client_callback(avahi::Client *c, avahi::ClientState state, void *) { + switch (state) { + case avahi::CLIENT_S_RUNNING: + create_services(c); + break; + case avahi::CLIENT_FAILURE: + BOOST_LOG(error) << "Client failure: "sv << avahi::strerror(avahi::client_errno(c)); + avahi::simple_poll_quit(poll.get()); + break; + case avahi::CLIENT_S_COLLISION: + case avahi::CLIENT_S_REGISTERING: + if (group) + avahi::entry_group_reset(group); + break; + case avahi::CLIENT_CONNECTING:; } } - fg.disable(); -} + class deinit_t: public ::platf::deinit_t { + public: + std::thread poll_thread; -void client_callback(avahi::Client *c, avahi::ClientState state, void *) { - switch(state) { - case avahi::CLIENT_S_RUNNING: - create_services(c); - break; - case avahi::CLIENT_FAILURE: - BOOST_LOG(error) << "Client failure: "sv << avahi::strerror(avahi::client_errno(c)); - avahi::simple_poll_quit(poll.get()); - break; - case avahi::CLIENT_S_COLLISION: - case avahi::CLIENT_S_REGISTERING: - if(group) - avahi::entry_group_reset(group); - break; - case avahi::CLIENT_CONNECTING:; - } -} + deinit_t(std::thread poll_thread): + poll_thread { std::move(poll_thread) } {} -class deinit_t : public ::platf::deinit_t { -public: - std::thread poll_thread; + ~deinit_t() override { + if (avahi::simple_poll_quit && poll) { + avahi::simple_poll_quit(poll.get()); + } - deinit_t(std::thread poll_thread) : poll_thread { std::move(poll_thread) } {} + if (poll_thread.joinable()) { + poll_thread.join(); + } + } + }; - ~deinit_t() override { - if(avahi::simple_poll_quit && poll) { - avahi::simple_poll_quit(poll.get()); + [[nodiscard]] std::unique_ptr<::platf::deinit_t> + start() { + if (avahi::init_client()) { + return nullptr; } - if(poll_thread.joinable()) { - poll_thread.join(); + int avhi_error; + + poll.reset(avahi::simple_poll_new()); + if (!poll) { + BOOST_LOG(error) << "Failed to create simple poll object."sv; + return nullptr; } + + name.reset(avahi::strdup(SERVICE_NAME)); + + client.reset( + avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error)); + + if (!client) { + BOOST_LOG(error) << "Failed to create client: "sv << avahi::strerror(avhi_error); + return nullptr; + } + + return std::make_unique(std::thread { avahi::simple_poll_loop, poll.get() }); } -}; - -[[nodiscard]] std::unique_ptr<::platf::deinit_t> start() { - if(avahi::init_client()) { - return nullptr; - } - - int avhi_error; - - poll.reset(avahi::simple_poll_new()); - if(!poll) { - BOOST_LOG(error) << "Failed to create simple poll object."sv; - return nullptr; - } - - name.reset(avahi::strdup(SERVICE_NAME)); - - client.reset( - avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error)); - - if(!client) { - BOOST_LOG(error) << "Failed to create client: "sv << avahi::strerror(avhi_error); - return nullptr; - } - - return std::make_unique(std::thread { avahi::simple_poll_loop, poll.get() }); -} -} // namespace platf::publish \ No newline at end of file +} // namespace platf::publish \ No newline at end of file diff --git a/src/platform/linux/vaapi.cpp b/src/platform/linux/vaapi.cpp index 1054d9fc..12281ac4 100644 --- a/src/platform/linux/vaapi.cpp +++ b/src/platform/linux/vaapi.cpp @@ -9,7 +9,8 @@ extern "C" { #if !VA_CHECK_VERSION(1, 9, 0) /* vaSyncBuffer stub allows Sunshine built against libva <2.9.0 to link against ffmpeg on libva 2.9.0 or later */ -VAStatus vaSyncBuffer( +VAStatus +vaSyncBuffer( VADisplay dpy, VABufferID buf_id, uint64_t timeout_ns) { @@ -30,112 +31,112 @@ using namespace std::literals; extern "C" struct AVBufferRef; namespace va { -constexpr auto SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 = 0x40000000; -constexpr auto EXPORT_SURFACE_WRITE_ONLY = 0x0002; -constexpr auto EXPORT_SURFACE_COMPOSED_LAYERS = 0x0008; + constexpr auto SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 = 0x40000000; + constexpr auto EXPORT_SURFACE_WRITE_ONLY = 0x0002; + constexpr auto EXPORT_SURFACE_COMPOSED_LAYERS = 0x0008; -using VADisplay = void *; -using VAStatus = int; -using VAGenericID = unsigned int; -using VASurfaceID = VAGenericID; + using VADisplay = void *; + using VAStatus = int; + using VAGenericID = unsigned int; + using VASurfaceID = VAGenericID; -struct DRMPRIMESurfaceDescriptor { - // VA Pixel format fourcc of the whole surface (VA_FOURCC_*). - uint32_t fourcc; + struct DRMPRIMESurfaceDescriptor { + // VA Pixel format fourcc of the whole surface (VA_FOURCC_*). + uint32_t fourcc; - uint32_t width; - uint32_t height; + uint32_t width; + uint32_t height; - // Number of distinct DRM objects making up the surface. - uint32_t num_objects; + // Number of distinct DRM objects making up the surface. + uint32_t num_objects; - struct { - // DRM PRIME file descriptor for this object. - // Needs to be closed manually - int fd; + struct { + // DRM PRIME file descriptor for this object. + // Needs to be closed manually + int fd; - /* + /* * Total size of this object (may include regions which are * not part of the surface). */ - uint32_t size; - // Format modifier applied to this object, not sure what that means - uint64_t drm_format_modifier; - } objects[4]; + uint32_t size; + // Format modifier applied to this object, not sure what that means + uint64_t drm_format_modifier; + } objects[4]; - // Number of layers making up the surface. - uint32_t num_layers; - struct { - // DRM format fourcc of this layer (DRM_FOURCC_*). - uint32_t drm_format; + // Number of layers making up the surface. + uint32_t num_layers; + struct { + // DRM format fourcc of this layer (DRM_FOURCC_*). + uint32_t drm_format; - // Number of planes in this layer. - uint32_t num_planes; + // Number of planes in this layer. + uint32_t num_planes; - // references objects --> DRMPRIMESurfaceDescriptor.objects[object_index[0]] - uint32_t object_index[4]; + // references objects --> DRMPRIMESurfaceDescriptor.objects[object_index[0]] + uint32_t object_index[4]; - // Offset within the object of each plane. - uint32_t offset[4]; + // Offset within the object of each plane. + uint32_t offset[4]; - // Pitch of each plane. - uint32_t pitch[4]; - } layers[4]; -}; + // Pitch of each plane. + uint32_t pitch[4]; + } layers[4]; + }; -/** Currently defined profiles */ -enum class profile_e { - // Profile ID used for video processing. - ProfileNone = -1, - MPEG2Simple = 0, - MPEG2Main = 1, - MPEG4Simple = 2, - MPEG4AdvancedSimple = 3, - MPEG4Main = 4, - H264Baseline = 5, - H264Main = 6, - H264High = 7, - VC1Simple = 8, - VC1Main = 9, - VC1Advanced = 10, - H263Baseline = 11, - JPEGBaseline = 12, - H264ConstrainedBaseline = 13, - VP8Version0_3 = 14, - H264MultiviewHigh = 15, - H264StereoHigh = 16, - HEVCMain = 17, - HEVCMain10 = 18, - VP9Profile0 = 19, - VP9Profile1 = 20, - VP9Profile2 = 21, - VP9Profile3 = 22, - HEVCMain12 = 23, - HEVCMain422_10 = 24, - HEVCMain422_12 = 25, - HEVCMain444 = 26, - HEVCMain444_10 = 27, - HEVCMain444_12 = 28, - HEVCSccMain = 29, - HEVCSccMain10 = 30, - HEVCSccMain444 = 31, - AV1Profile0 = 32, - AV1Profile1 = 33, - HEVCSccMain444_10 = 34, + /** Currently defined profiles */ + enum class profile_e { + // Profile ID used for video processing. + ProfileNone = -1, + MPEG2Simple = 0, + MPEG2Main = 1, + MPEG4Simple = 2, + MPEG4AdvancedSimple = 3, + MPEG4Main = 4, + H264Baseline = 5, + H264Main = 6, + H264High = 7, + VC1Simple = 8, + VC1Main = 9, + VC1Advanced = 10, + H263Baseline = 11, + JPEGBaseline = 12, + H264ConstrainedBaseline = 13, + VP8Version0_3 = 14, + H264MultiviewHigh = 15, + H264StereoHigh = 16, + HEVCMain = 17, + HEVCMain10 = 18, + VP9Profile0 = 19, + VP9Profile1 = 20, + VP9Profile2 = 21, + VP9Profile3 = 22, + HEVCMain12 = 23, + HEVCMain422_10 = 24, + HEVCMain422_12 = 25, + HEVCMain444 = 26, + HEVCMain444_10 = 27, + HEVCMain444_12 = 28, + HEVCSccMain = 29, + HEVCSccMain10 = 30, + HEVCSccMain444 = 31, + AV1Profile0 = 32, + AV1Profile1 = 33, + HEVCSccMain444_10 = 34, - // Profile ID used for protected video playback. - Protected = 35 -}; + // Profile ID used for protected video playback. + Protected = 35 + }; -enum class entry_e { - VLD = 1, - IZZ = 2, - IDCT = 3, - MoComp = 4, - Deblocking = 5, - EncSlice = 6, /* slice level encode */ - EncPicture = 7, /* pictuer encode, JPEG, etc */ - /* + enum class entry_e { + VLD = 1, + IZZ = 2, + IDCT = 3, + MoComp = 4, + Deblocking = 5, + EncSlice = 6, /* slice level encode */ + EncPicture = 7, /* pictuer encode, JPEG, etc */ + /* * For an implementation that supports a low power/high performance variant * for slice level encode, it can choose to expose the * VAEntrypointEncSliceLP entrypoint. Certain encoding tools may not be @@ -143,9 +144,9 @@ enum class entry_e { * application can query the encoding configuration attributes to find * out more details if this entrypoint is supported. */ - EncSliceLP = 8, - VideoProc = 10, /**< Video pre/post-processing. */ - /** + EncSliceLP = 8, + VideoProc = 10, /**< Video pre/post-processing. */ + /** * \brief FEI * * The purpose of FEI (Flexible Encoding Infrastructure) is to allow applications to @@ -161,8 +162,8 @@ enum class entry_e { * If separate PAK is set, two extra input buffers * (VAEncFEIMVBufferType, VAEncFEIMBModeBufferType) are needed for PAK input. **/ - FEI = 11, - /** + FEI = 11, + /** * \brief Stats * * A pre-processing function for getting some statistics and motion vectors is added, @@ -178,488 +179,502 @@ enum class entry_e { * (VAStatsStatisticsBufferType, VAStatsStatisticsBottomFieldBufferType (for interlace only) * and VAStatsMVBufferType) are needed for this entry point. **/ - Stats = 12, - /** + Stats = 12, + /** * \brief ProtectedTEEComm * * A function for communicating with TEE (Trusted Execution Environment). **/ - ProtectedTEEComm = 13, - /** + ProtectedTEEComm = 13, + /** * \brief ProtectedContent * * A function for protected content to decrypt encrypted content. **/ - ProtectedContent = 14, -}; - - -typedef VAStatus (*queryConfigEntrypoints_fn)(VADisplay dpy, profile_e profile, entry_e *entrypoint_list, int *num_entrypoints); -typedef int (*maxNumEntrypoints_fn)(VADisplay dpy); -typedef VADisplay (*getDisplayDRM_fn)(int fd); -typedef VAStatus (*terminate_fn)(VADisplay dpy); -typedef VAStatus (*initialize_fn)(VADisplay dpy, int *major_version, int *minor_version); -typedef const char *(*errorStr_fn)(VAStatus error_status); -typedef void (*VAMessageCallback)(void *user_context, const char *message); -typedef VAMessageCallback (*setErrorCallback_fn)(VADisplay dpy, VAMessageCallback callback, void *user_context); -typedef VAMessageCallback (*setInfoCallback_fn)(VADisplay dpy, VAMessageCallback callback, void *user_context); -typedef const char *(*queryVendorString_fn)(VADisplay dpy); -typedef VAStatus (*exportSurfaceHandle_fn)( - VADisplay dpy, VASurfaceID surface_id, - uint32_t mem_type, uint32_t flags, - void *descriptor); - -static maxNumEntrypoints_fn maxNumEntrypoints; -static queryConfigEntrypoints_fn queryConfigEntrypoints; -static getDisplayDRM_fn getDisplayDRM; -static terminate_fn terminate; -static initialize_fn initialize; -static errorStr_fn errorStr; -static setErrorCallback_fn setErrorCallback; -static setInfoCallback_fn setInfoCallback; -static queryVendorString_fn queryVendorString; -static exportSurfaceHandle_fn exportSurfaceHandle; - -using display_t = util::dyn_safe_ptr_v2; - -int init_main_va() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if(funcs_loaded) return 0; - - if(!handle) { - handle = dyn::handle({ "libva.so.2", "libva.so" }); - if(!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *)&maxNumEntrypoints, "vaMaxNumEntrypoints" }, - { (dyn::apiproc *)&queryConfigEntrypoints, "vaQueryConfigEntrypoints" }, - { (dyn::apiproc *)&terminate, "vaTerminate" }, - { (dyn::apiproc *)&initialize, "vaInitialize" }, - { (dyn::apiproc *)&errorStr, "vaErrorStr" }, - { (dyn::apiproc *)&setErrorCallback, "vaSetErrorCallback" }, - { (dyn::apiproc *)&setInfoCallback, "vaSetInfoCallback" }, - { (dyn::apiproc *)&queryVendorString, "vaQueryVendorString" }, - { (dyn::apiproc *)&exportSurfaceHandle, "vaExportSurfaceHandle" }, + ProtectedContent = 14, }; - if(dyn::load(handle, funcs)) { - return -1; - } + typedef VAStatus (*queryConfigEntrypoints_fn)(VADisplay dpy, profile_e profile, entry_e *entrypoint_list, int *num_entrypoints); + typedef int (*maxNumEntrypoints_fn)(VADisplay dpy); + typedef VADisplay (*getDisplayDRM_fn)(int fd); + typedef VAStatus (*terminate_fn)(VADisplay dpy); + typedef VAStatus (*initialize_fn)(VADisplay dpy, int *major_version, int *minor_version); + typedef const char *(*errorStr_fn)(VAStatus error_status); + typedef void (*VAMessageCallback)(void *user_context, const char *message); + typedef VAMessageCallback (*setErrorCallback_fn)(VADisplay dpy, VAMessageCallback callback, void *user_context); + typedef VAMessageCallback (*setInfoCallback_fn)(VADisplay dpy, VAMessageCallback callback, void *user_context); + typedef const char *(*queryVendorString_fn)(VADisplay dpy); + typedef VAStatus (*exportSurfaceHandle_fn)( + VADisplay dpy, VASurfaceID surface_id, + uint32_t mem_type, uint32_t flags, + void *descriptor); - funcs_loaded = true; - return 0; -} + static maxNumEntrypoints_fn maxNumEntrypoints; + static queryConfigEntrypoints_fn queryConfigEntrypoints; + static getDisplayDRM_fn getDisplayDRM; + static terminate_fn terminate; + static initialize_fn initialize; + static errorStr_fn errorStr; + static setErrorCallback_fn setErrorCallback; + static setInfoCallback_fn setInfoCallback; + static queryVendorString_fn queryVendorString; + static exportSurfaceHandle_fn exportSurfaceHandle; -int init() { - if(init_main_va()) { - return -1; - } + using display_t = util::dyn_safe_ptr_v2; - static void *handle { nullptr }; - static bool funcs_loaded = false; + int + init_main_va() { + static void *handle { nullptr }; + static bool funcs_loaded = false; - if(funcs_loaded) return 0; + if (funcs_loaded) return 0; - if(!handle) { - handle = dyn::handle({ "libva-drm.so.2", "libva-drm.so" }); - if(!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *)&getDisplayDRM, "vaGetDisplayDRM" }, - }; - - if(dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; -} - -int vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf); - -class va_t : public platf::hwdevice_t { -public: - int init(int in_width, int in_height, file_t &&render_device) { - file = std::move(render_device); - - if(!va::initialize || !gbm::create_device) { - if(!va::initialize) BOOST_LOG(warning) << "libva not initialized"sv; - if(!gbm::create_device) BOOST_LOG(warning) << "libgbm not initialized"sv; - return -1; - } - - this->data = (void *)vaapi_make_hwdevice_ctx; - - gbm.reset(gbm::create_device(file.el)); - if(!gbm) { - char string[1024]; - BOOST_LOG(error) << "Couldn't create GBM device: ["sv << strerror_r(errno, string, sizeof(string)) << ']'; - return -1; - } - - display = egl::make_display(gbm.get()); - if(!display) { - return -1; - } - - auto ctx_opt = egl::make_ctx(display.get()); - if(!ctx_opt) { - return -1; - } - - ctx = std::move(*ctx_opt); - - width = in_width; - height = in_height; - - return 0; - } - - int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { - this->hwframe.reset(frame); - this->frame = frame; - - if(!frame->buf[0]) { - if(av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) { - BOOST_LOG(error) << "Couldn't get hwframe for VAAPI"sv; + if (!handle) { + handle = dyn::handle({ "libva.so.2", "libva.so" }); + if (!handle) { return -1; } } - va::DRMPRIMESurfaceDescriptor prime; - va::VASurfaceID surface = (std::uintptr_t)frame->data[3]; - - auto status = va::exportSurfaceHandle( - this->va_display, - surface, - va::SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, - va::EXPORT_SURFACE_WRITE_ONLY | va::EXPORT_SURFACE_COMPOSED_LAYERS, - &prime); - if(status) { - - BOOST_LOG(error) << "Couldn't export va surface handle: ["sv << (int)surface << "]: "sv << va::errorStr(status); + std::vector> funcs { + { (dyn::apiproc *) &maxNumEntrypoints, "vaMaxNumEntrypoints" }, + { (dyn::apiproc *) &queryConfigEntrypoints, "vaQueryConfigEntrypoints" }, + { (dyn::apiproc *) &terminate, "vaTerminate" }, + { (dyn::apiproc *) &initialize, "vaInitialize" }, + { (dyn::apiproc *) &errorStr, "vaErrorStr" }, + { (dyn::apiproc *) &setErrorCallback, "vaSetErrorCallback" }, + { (dyn::apiproc *) &setInfoCallback, "vaSetInfoCallback" }, + { (dyn::apiproc *) &queryVendorString, "vaQueryVendorString" }, + { (dyn::apiproc *) &exportSurfaceHandle, "vaExportSurfaceHandle" }, + }; + if (dyn::load(handle, funcs)) { return -1; } - // Keep track of file descriptors - std::array fds; - for(int x = 0; x < prime.num_objects; ++x) { - fds[x] = prime.objects[x].fd; - } - - auto nv12_opt = egl::import_target( - display.get(), - std::move(fds), - { (int)prime.width, - (int)prime.height, - { prime.objects[prime.layers[0].object_index[0]].fd, -1, -1, -1 }, - 0, - 0, - { prime.layers[0].pitch[0] }, - { prime.layers[0].offset[0] } }, - { (int)prime.width / 2, - (int)prime.height / 2, - { prime.objects[prime.layers[0].object_index[1]].fd, -1, -1, -1 }, - 0, - 0, - { prime.layers[0].pitch[1] }, - { prime.layers[0].offset[1] } }); - - if(!nv12_opt) { - return -1; - } - - auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height); - if(!sws_opt) { - return -1; - } - - this->sws = std::move(*sws_opt); - this->nv12 = std::move(*nv12_opt); - + funcs_loaded = true; return 0; } - void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override { - sws.set_colorspace(colorspace, color_range); - } + int + init() { + if (init_main_va()) { + return -1; + } - va::display_t::pointer va_display; - file_t file; + static void *handle { nullptr }; + static bool funcs_loaded = false; - gbm::gbm_t gbm; - egl::display_t display; - egl::ctx_t ctx; + if (funcs_loaded) return 0; - // This must be destroyed before display_t to ensure the GPU - // driver is still loaded when vaDestroySurfaces() is called. - frame_t hwframe; + if (!handle) { + handle = dyn::handle({ "libva-drm.so.2", "libva-drm.so" }); + if (!handle) { + return -1; + } + } - egl::sws_t sws; - egl::nv12_t nv12; + std::vector> funcs { + { (dyn::apiproc *) &getDisplayDRM, "vaGetDisplayDRM" }, + }; - int width, height; -}; + if (dyn::load(handle, funcs)) { + return -1; + } -class va_ram_t : public va_t { -public: - int convert(platf::img_t &img) override { - sws.load_ram(img); - - sws.convert(nv12->buf); + funcs_loaded = true; return 0; } -}; -class va_vram_t : public va_t { -public: - int convert(platf::img_t &img) override { - auto &descriptor = (egl::img_descriptor_t &)img; + int + vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf); - if(descriptor.sequence > sequence) { - sequence = descriptor.sequence; + class va_t: public platf::hwdevice_t { + public: + int + init(int in_width, int in_height, file_t &&render_device) { + file = std::move(render_device); - rgb = egl::rgb_t {}; - - auto rgb_opt = egl::import_source(display.get(), descriptor.sd); - - if(!rgb_opt) { + if (!va::initialize || !gbm::create_device) { + if (!va::initialize) BOOST_LOG(warning) << "libva not initialized"sv; + if (!gbm::create_device) BOOST_LOG(warning) << "libgbm not initialized"sv; return -1; } - rgb = std::move(*rgb_opt); + this->data = (void *) vaapi_make_hwdevice_ctx; + + gbm.reset(gbm::create_device(file.el)); + if (!gbm) { + char string[1024]; + BOOST_LOG(error) << "Couldn't create GBM device: ["sv << strerror_r(errno, string, sizeof(string)) << ']'; + return -1; + } + + display = egl::make_display(gbm.get()); + if (!display) { + return -1; + } + + auto ctx_opt = egl::make_ctx(display.get()); + if (!ctx_opt) { + return -1; + } + + ctx = std::move(*ctx_opt); + + width = in_width; + height = in_height; + + return 0; } - sws.load_vram(descriptor, offset_x, offset_y, rgb->tex[0]); + int + set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { + this->hwframe.reset(frame); + this->frame = frame; - sws.convert(nv12->buf); - return 0; - } + if (!frame->buf[0]) { + if (av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) { + BOOST_LOG(error) << "Couldn't get hwframe for VAAPI"sv; + return -1; + } + } - int init(int in_width, int in_height, file_t &&render_device, int offset_x, int offset_y) { - if(va_t::init(in_width, in_height, std::move(render_device))) { - return -1; + va::DRMPRIMESurfaceDescriptor prime; + va::VASurfaceID surface = (std::uintptr_t) frame->data[3]; + + auto status = va::exportSurfaceHandle( + this->va_display, + surface, + va::SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, + va::EXPORT_SURFACE_WRITE_ONLY | va::EXPORT_SURFACE_COMPOSED_LAYERS, + &prime); + if (status) { + BOOST_LOG(error) << "Couldn't export va surface handle: ["sv << (int) surface << "]: "sv << va::errorStr(status); + + return -1; + } + + // Keep track of file descriptors + std::array fds; + for (int x = 0; x < prime.num_objects; ++x) { + fds[x] = prime.objects[x].fd; + } + + auto nv12_opt = egl::import_target( + display.get(), + std::move(fds), + { (int) prime.width, + (int) prime.height, + { prime.objects[prime.layers[0].object_index[0]].fd, -1, -1, -1 }, + 0, + 0, + { prime.layers[0].pitch[0] }, + { prime.layers[0].offset[0] } }, + { (int) prime.width / 2, + (int) prime.height / 2, + { prime.objects[prime.layers[0].object_index[1]].fd, -1, -1, -1 }, + 0, + 0, + { prime.layers[0].pitch[1] }, + { prime.layers[0].offset[1] } }); + + if (!nv12_opt) { + return -1; + } + + auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height); + if (!sws_opt) { + return -1; + } + + this->sws = std::move(*sws_opt); + this->nv12 = std::move(*nv12_opt); + + return 0; } - sequence = 0; + void + set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override { + sws.set_colorspace(colorspace, color_range); + } - this->offset_x = offset_x; - this->offset_y = offset_y; + va::display_t::pointer va_display; + file_t file; - return 0; - } + gbm::gbm_t gbm; + egl::display_t display; + egl::ctx_t ctx; - std::uint64_t sequence; - egl::rgb_t rgb; + // This must be destroyed before display_t to ensure the GPU + // driver is still loaded when vaDestroySurfaces() is called. + frame_t hwframe; - int offset_x, offset_y; -}; + egl::sws_t sws; + egl::nv12_t nv12; -/** + int width, height; + }; + + class va_ram_t: public va_t { + public: + int + convert(platf::img_t &img) override { + sws.load_ram(img); + + sws.convert(nv12->buf); + return 0; + } + }; + + class va_vram_t: public va_t { + public: + int + convert(platf::img_t &img) override { + auto &descriptor = (egl::img_descriptor_t &) img; + + if (descriptor.sequence > sequence) { + sequence = descriptor.sequence; + + rgb = egl::rgb_t {}; + + auto rgb_opt = egl::import_source(display.get(), descriptor.sd); + + if (!rgb_opt) { + return -1; + } + + rgb = std::move(*rgb_opt); + } + + sws.load_vram(descriptor, offset_x, offset_y, rgb->tex[0]); + + sws.convert(nv12->buf); + return 0; + } + + int + init(int in_width, int in_height, file_t &&render_device, int offset_x, int offset_y) { + if (va_t::init(in_width, in_height, std::move(render_device))) { + return -1; + } + + sequence = 0; + + this->offset_x = offset_x; + this->offset_y = offset_y; + + return 0; + } + + std::uint64_t sequence; + egl::rgb_t rgb; + + int offset_x, offset_y; + }; + + /** * This is a private structure of FFmpeg, I need this to manually create * a VAAPI hardware context * * xdisplay will not be used internally by FFmpeg */ -typedef struct VAAPIDevicePriv { - union { - void *xdisplay; - int fd; - } drm; - int drm_fd; -} VAAPIDevicePriv; + typedef struct VAAPIDevicePriv { + union { + void *xdisplay; + int fd; + } drm; + int drm_fd; + } VAAPIDevicePriv; -/** + /** * VAAPI connection details. * * Allocated as AVHWDeviceContext.hwctx */ -typedef struct AVVAAPIDeviceContext { - /** + typedef struct AVVAAPIDeviceContext { + /** * The VADisplay handle, to be filled by the user. */ - va::VADisplay display; - /** + va::VADisplay display; + /** * Driver quirks to apply - this is filled by av_hwdevice_ctx_init(), * with reference to a table of known drivers, unless the * AV_VAAPI_DRIVER_QUIRK_USER_SET bit is already present. The user * may need to refer to this field when performing any later * operations using VAAPI with the same VADisplay. */ - unsigned int driver_quirks; -} AVVAAPIDeviceContext; + unsigned int driver_quirks; + } AVVAAPIDeviceContext; -static void __log(void *level, const char *msg) { - BOOST_LOG(*(boost::log::sources::severity_logger *)level) << msg; -} - -int vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf) { - if(!va::initialize) { - BOOST_LOG(warning) << "libva not loaded"sv; - return -1; + static void + __log(void *level, const char *msg) { + BOOST_LOG(*(boost::log::sources::severity_logger *) level) << msg; } - if(!va::getDisplayDRM) { - BOOST_LOG(warning) << "libva-drm not loaded"sv; - return -1; + int + vaapi_make_hwdevice_ctx(platf::hwdevice_t *base, AVBufferRef **hw_device_buf) { + if (!va::initialize) { + BOOST_LOG(warning) << "libva not loaded"sv; + return -1; + } + + if (!va::getDisplayDRM) { + BOOST_LOG(warning) << "libva-drm not loaded"sv; + return -1; + } + + auto va = (va::va_t *) base; + auto fd = dup(va->file.el); + + auto *priv = (VAAPIDevicePriv *) av_mallocz(sizeof(VAAPIDevicePriv)); + priv->drm_fd = fd; + priv->drm.fd = fd; + + auto fg = util::fail_guard([fd, priv]() { + close(fd); + av_free(priv); + }); + + va::display_t display { va::getDisplayDRM(fd) }; + if (!display) { + auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str(); + + BOOST_LOG(error) << "Couldn't open a va display from DRM with device: "sv << render_device; + return -1; + } + + va->va_display = display.get(); + + va::setErrorCallback(display.get(), __log, &error); + va::setErrorCallback(display.get(), __log, &info); + + int major, minor; + auto status = va::initialize(display.get(), &major, &minor); + if (status) { + BOOST_LOG(error) << "Couldn't initialize va display: "sv << va::errorStr(status); + return -1; + } + + BOOST_LOG(debug) << "vaapi vendor: "sv << va::queryVendorString(display.get()); + + *hw_device_buf = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI); + auto ctx = (AVVAAPIDeviceContext *) ((AVHWDeviceContext *) (*hw_device_buf)->data)->hwctx; + ctx->display = display.release(); + + fg.disable(); + + auto err = av_hwdevice_ctx_init(*hw_device_buf); + if (err) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + BOOST_LOG(error) << "Failed to create FFMpeg hardware device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + + return err; + } + + return 0; } - auto va = (va::va_t *)base; - auto fd = dup(va->file.el); + static bool + query(display_t::pointer display, profile_e profile) { + std::vector entrypoints; + entrypoints.resize(maxNumEntrypoints(display)); - auto *priv = (VAAPIDevicePriv *)av_mallocz(sizeof(VAAPIDevicePriv)); - priv->drm_fd = fd; - priv->drm.fd = fd; + int count; + auto status = queryConfigEntrypoints(display, profile, entrypoints.data(), &count); + if (status) { + BOOST_LOG(error) << "Couldn't query entrypoints: "sv << va::errorStr(status); + return false; + } + entrypoints.resize(count); - auto fg = util::fail_guard([fd, priv]() { - close(fd); - av_free(priv); - }); + for (auto entrypoint : entrypoints) { + if (entrypoint == entry_e::EncSlice || entrypoint == entry_e::EncSliceLP) { + return true; + } + } - va::display_t display { va::getDisplayDRM(fd) }; - if(!display) { + return false; + } + + bool + validate(int fd) { + if (init()) { + return false; + } + + va::display_t display { va::getDisplayDRM(fd) }; + if (!display) { + char string[1024]; + + auto bytes = readlink(("/proc/self/fd/" + std::to_string(fd)).c_str(), string, sizeof(string)); + + std::string_view render_device { string, (std::size_t) bytes }; + + BOOST_LOG(error) << "Couldn't open a va display from DRM with device: "sv << render_device; + return false; + } + + int major, minor; + auto status = initialize(display.get(), &major, &minor); + if (status) { + BOOST_LOG(error) << "Couldn't initialize va display: "sv << va::errorStr(status); + return false; + } + + if (!query(display.get(), profile_e::H264Main)) { + return false; + } + + if (config::video.hevc_mode > 1 && !query(display.get(), profile_e::HEVCMain)) { + return false; + } + + if (config::video.hevc_mode > 2 && !query(display.get(), profile_e::HEVCMain10)) { + return false; + } + + return true; + } + + std::shared_ptr + make_hwdevice(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram) { + if (vram) { + auto egl = std::make_shared(); + if (egl->init(width, height, std::move(card), offset_x, offset_y)) { + return nullptr; + } + + return egl; + } + + else { + auto egl = std::make_shared(); + if (egl->init(width, height, std::move(card))) { + return nullptr; + } + + return egl; + } + } + + std::shared_ptr + make_hwdevice(int width, int height, int offset_x, int offset_y, bool vram) { auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str(); - BOOST_LOG(error) << "Couldn't open a va display from DRM with device: "sv << render_device; - return -1; - } + file_t file = open(render_device, O_RDWR); + if (file.el < 0) { + char string[1024]; + BOOST_LOG(error) << "Couldn't open "sv << render_device << ": " << strerror_r(errno, string, sizeof(string)); - va->va_display = display.get(); - - va::setErrorCallback(display.get(), __log, &error); - va::setErrorCallback(display.get(), __log, &info); - - int major, minor; - auto status = va::initialize(display.get(), &major, &minor); - if(status) { - BOOST_LOG(error) << "Couldn't initialize va display: "sv << va::errorStr(status); - return -1; - } - - BOOST_LOG(debug) << "vaapi vendor: "sv << va::queryVendorString(display.get()); - - *hw_device_buf = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI); - auto ctx = (AVVAAPIDeviceContext *)((AVHWDeviceContext *)(*hw_device_buf)->data)->hwctx; - ctx->display = display.release(); - - fg.disable(); - - auto err = av_hwdevice_ctx_init(*hw_device_buf); - if(err) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) << "Failed to create FFMpeg hardware device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); - - return err; - } - - return 0; -} - -static bool query(display_t::pointer display, profile_e profile) { - std::vector entrypoints; - entrypoints.resize(maxNumEntrypoints(display)); - - int count; - auto status = queryConfigEntrypoints(display, profile, entrypoints.data(), &count); - if(status) { - BOOST_LOG(error) << "Couldn't query entrypoints: "sv << va::errorStr(status); - return false; - } - entrypoints.resize(count); - - for(auto entrypoint : entrypoints) { - if(entrypoint == entry_e::EncSlice || entrypoint == entry_e::EncSliceLP) { - return true; - } - } - - return false; -} - -bool validate(int fd) { - if(init()) { - return false; - } - - va::display_t display { va::getDisplayDRM(fd) }; - if(!display) { - char string[1024]; - - auto bytes = readlink(("/proc/self/fd/" + std::to_string(fd)).c_str(), string, sizeof(string)); - - std::string_view render_device { string, (std::size_t)bytes }; - - BOOST_LOG(error) << "Couldn't open a va display from DRM with device: "sv << render_device; - return false; - } - - int major, minor; - auto status = initialize(display.get(), &major, &minor); - if(status) { - BOOST_LOG(error) << "Couldn't initialize va display: "sv << va::errorStr(status); - return false; - } - - if(!query(display.get(), profile_e::H264Main)) { - return false; - } - - if(config::video.hevc_mode > 1 && !query(display.get(), profile_e::HEVCMain)) { - return false; - } - - if(config::video.hevc_mode > 2 && !query(display.get(), profile_e::HEVCMain10)) { - return false; - } - - return true; -} - -std::shared_ptr make_hwdevice(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram) { - if(vram) { - auto egl = std::make_shared(); - if(egl->init(width, height, std::move(card), offset_x, offset_y)) { return nullptr; } - return egl; + return make_hwdevice(width, height, std::move(file), offset_x, offset_y, vram); } - else { - auto egl = std::make_shared(); - if(egl->init(width, height, std::move(card))) { - return nullptr; - } - - return egl; + std::shared_ptr + make_hwdevice(int width, int height, bool vram) { + return make_hwdevice(width, height, 0, 0, vram); } -} - -std::shared_ptr make_hwdevice(int width, int height, int offset_x, int offset_y, bool vram) { - auto render_device = config::video.adapter_name.empty() ? "/dev/dri/renderD128" : config::video.adapter_name.c_str(); - - file_t file = open(render_device, O_RDWR); - if(file.el < 0) { - char string[1024]; - BOOST_LOG(error) << "Couldn't open "sv << render_device << ": " << strerror_r(errno, string, sizeof(string)); - - return nullptr; - } - - return make_hwdevice(width, height, std::move(file), offset_x, offset_y, vram); -} - -std::shared_ptr make_hwdevice(int width, int height, bool vram) { - return make_hwdevice(width, height, 0, 0, vram); -} -} // namespace va +} // namespace va diff --git a/src/platform/linux/vaapi.h b/src/platform/linux/vaapi.h index 27c97a7f..7f03c5a7 100644 --- a/src/platform/linux/vaapi.h +++ b/src/platform/linux/vaapi.h @@ -5,23 +5,28 @@ #include "src/platform/common.h" namespace egl { -struct surface_descriptor_t; + struct surface_descriptor_t; } namespace va { -/** + /** * Width --> Width of the image * Height --> Height of the image * offset_x --> Horizontal offset of the image in the texture * offset_y --> Vertical offset of the image in the texture * file_t card --> The file descriptor of the render device used for encoding */ -std::shared_ptr make_hwdevice(int width, int height, bool vram); -std::shared_ptr make_hwdevice(int width, int height, int offset_x, int offset_y, bool vram); -std::shared_ptr make_hwdevice(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram); + std::shared_ptr + make_hwdevice(int width, int height, bool vram); + std::shared_ptr + make_hwdevice(int width, int height, int offset_x, int offset_y, bool vram); + std::shared_ptr + make_hwdevice(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram); -// Ensure the render device pointed to by fd is capable of encoding h264 with the hevc_mode configured -bool validate(int fd); + // Ensure the render device pointed to by fd is capable of encoding h264 with the hevc_mode configured + bool + validate(int fd); -int init(); -} // namespace va + int + init(); +} // namespace va #endif \ No newline at end of file diff --git a/src/platform/linux/wayland.cpp b/src/platform/linux/wayland.cpp index d9da7881..5bd207cd 100644 --- a/src/platform/linux/wayland.cpp +++ b/src/platform/linux/wayland.cpp @@ -21,45 +21,49 @@ using namespace std::literals; namespace wl { -// Helper to call C++ method from wayland C callback -template -static auto classCall(void *data, Params... params) -> decltype(((*reinterpret_cast(data)).*m)(params...)) { - return ((*reinterpret_cast(data)).*m)(params...); -} + // Helper to call C++ method from wayland C callback + template + static auto + classCall(void *data, Params... params) -> decltype(((*reinterpret_cast(data)).*m)(params...)) { + return ((*reinterpret_cast(data)).*m)(params...); + } #define CLASS_CALL(c, m) classCall -int display_t::init(const char *display_name) { - if(!display_name) { - display_name = std::getenv("WAYLAND_DISPLAY"); + int + display_t::init(const char *display_name) { + if (!display_name) { + display_name = std::getenv("WAYLAND_DISPLAY"); + } + + if (!display_name) { + BOOST_LOG(error) << "Environment variable WAYLAND_DISPLAY has not been defined"sv; + return -1; + } + + display_internal.reset(wl_display_connect(display_name)); + if (!display_internal) { + BOOST_LOG(error) << "Couldn't connect to Wayland display: "sv << display_name; + return -1; + } + + BOOST_LOG(info) << "Found display ["sv << display_name << ']'; + + return 0; } - if(!display_name) { - BOOST_LOG(error) << "Environment variable WAYLAND_DISPLAY has not been defined"sv; - return -1; + void + display_t::roundtrip() { + wl_display_roundtrip(display_internal.get()); } - display_internal.reset(wl_display_connect(display_name)); - if(!display_internal) { - BOOST_LOG(error) << "Couldn't connect to Wayland display: "sv << display_name; - return -1; + wl_registry * + display_t::registry() { + return wl_display_get_registry(display_internal.get()); } - BOOST_LOG(info) << "Found display ["sv << display_name << ']'; - - return 0; -} - -void display_t::roundtrip() { - wl_display_roundtrip(display_internal.get()); -} - -wl_registry *display_t::registry() { - return wl_display_get_registry(display_internal.get()); -} - -inline monitor_t::monitor_t(wl_output *output) - : output { output }, listener { + inline monitor_t::monitor_t(wl_output *output): + output { output }, listener { &CLASS_CALL(monitor_t, xdg_position), &CLASS_CALL(monitor_t, xdg_size), &CLASS_CALL(monitor_t, xdg_done), @@ -67,208 +71,226 @@ inline monitor_t::monitor_t(wl_output *output) &CLASS_CALL(monitor_t, xdg_description) } {} -inline void monitor_t::xdg_name(zxdg_output_v1 *, const char *name) { - this->name = name; + inline void + monitor_t::xdg_name(zxdg_output_v1 *, const char *name) { + this->name = name; - BOOST_LOG(info) << "Name: "sv << this->name; -} + BOOST_LOG(info) << "Name: "sv << this->name; + } -void monitor_t::xdg_description(zxdg_output_v1 *, const char *description) { - this->description = description; + void + monitor_t::xdg_description(zxdg_output_v1 *, const char *description) { + this->description = description; - BOOST_LOG(info) << "Found monitor: "sv << this->description; -} + BOOST_LOG(info) << "Found monitor: "sv << this->description; + } -void monitor_t::xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y) { - viewport.offset_x = x; - viewport.offset_y = y; + void + monitor_t::xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y) { + viewport.offset_x = x; + viewport.offset_y = y; - BOOST_LOG(info) << "Offset: "sv << x << 'x' << y; -} + BOOST_LOG(info) << "Offset: "sv << x << 'x' << y; + } -void monitor_t::xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height) { - viewport.width = width; - viewport.height = height; + void + monitor_t::xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height) { + viewport.width = width; + viewport.height = height; - BOOST_LOG(info) << "Resolution: "sv << width << 'x' << height; -} + BOOST_LOG(info) << "Resolution: "sv << width << 'x' << height; + } -void monitor_t::xdg_done(zxdg_output_v1 *) { - BOOST_LOG(info) << "All info about monitor ["sv << name << "] has been send"sv; -} + void + monitor_t::xdg_done(zxdg_output_v1 *) { + BOOST_LOG(info) << "All info about monitor ["sv << name << "] has been send"sv; + } -void monitor_t::listen(zxdg_output_manager_v1 *output_manager) { - auto xdg_output = zxdg_output_manager_v1_get_xdg_output(output_manager, output); - zxdg_output_v1_add_listener(xdg_output, &listener, this); -} + void + monitor_t::listen(zxdg_output_manager_v1 *output_manager) { + auto xdg_output = zxdg_output_manager_v1_get_xdg_output(output_manager, output); + zxdg_output_v1_add_listener(xdg_output, &listener, this); + } -interface_t::interface_t() noexcept - : output_manager { nullptr }, listener { + interface_t::interface_t() noexcept + : + output_manager { nullptr }, + listener { &CLASS_CALL(interface_t, add_interface), &CLASS_CALL(interface_t, del_interface) } {} -void interface_t::listen(wl_registry *registry) { - wl_registry_add_listener(registry, &listener, this); -} - -void interface_t::add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version) { - BOOST_LOG(debug) << "Available interface: "sv << interface << '(' << id << ") version "sv << version; - - if(!std::strcmp(interface, wl_output_interface.name)) { - BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; - monitors.emplace_back( - std::make_unique( - (wl_output *)wl_registry_bind(registry, id, &wl_output_interface, version))); + void + interface_t::listen(wl_registry *registry) { + wl_registry_add_listener(registry, &listener, this); } - else if(!std::strcmp(interface, zxdg_output_manager_v1_interface.name)) { - BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; - output_manager = (zxdg_output_manager_v1 *)wl_registry_bind(registry, id, &zxdg_output_manager_v1_interface, version); - this->interface[XDG_OUTPUT] = true; + void + interface_t::add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version) { + BOOST_LOG(debug) << "Available interface: "sv << interface << '(' << id << ") version "sv << version; + + if (!std::strcmp(interface, wl_output_interface.name)) { + BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; + monitors.emplace_back( + std::make_unique( + (wl_output *) wl_registry_bind(registry, id, &wl_output_interface, version))); + } + else if (!std::strcmp(interface, zxdg_output_manager_v1_interface.name)) { + BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; + output_manager = (zxdg_output_manager_v1 *) wl_registry_bind(registry, id, &zxdg_output_manager_v1_interface, version); + + this->interface[XDG_OUTPUT] = true; + } + else if (!std::strcmp(interface, zwlr_export_dmabuf_manager_v1_interface.name)) { + BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; + dmabuf_manager = (zwlr_export_dmabuf_manager_v1 *) wl_registry_bind(registry, id, &zwlr_export_dmabuf_manager_v1_interface, version); + + this->interface[WLR_EXPORT_DMABUF] = true; + } } - else if(!std::strcmp(interface, zwlr_export_dmabuf_manager_v1_interface.name)) { - BOOST_LOG(info) << "Found interface: "sv << interface << '(' << id << ") version "sv << version; - dmabuf_manager = (zwlr_export_dmabuf_manager_v1 *)wl_registry_bind(registry, id, &zwlr_export_dmabuf_manager_v1_interface, version); - this->interface[WLR_EXPORT_DMABUF] = true; + void + interface_t::del_interface(wl_registry *registry, uint32_t id) { + BOOST_LOG(info) << "Delete: "sv << id; } -} -void interface_t::del_interface(wl_registry *registry, uint32_t id) { - BOOST_LOG(info) << "Delete: "sv << id; -} - -dmabuf_t::dmabuf_t() - : status { READY }, frames {}, current_frame { &frames[0] }, listener { + dmabuf_t::dmabuf_t(): + status { READY }, frames {}, current_frame { &frames[0] }, listener { &CLASS_CALL(dmabuf_t, frame), &CLASS_CALL(dmabuf_t, object), &CLASS_CALL(dmabuf_t, ready), &CLASS_CALL(dmabuf_t, cancel) } { -} - -void dmabuf_t::listen(zwlr_export_dmabuf_manager_v1 *dmabuf_manager, wl_output *output, bool blend_cursor) { - auto frame = zwlr_export_dmabuf_manager_v1_capture_output(dmabuf_manager, blend_cursor, output); - zwlr_export_dmabuf_frame_v1_add_listener(frame, &listener, this); - - status = WAITING; -} - -dmabuf_t::~dmabuf_t() { - for(auto &frame : frames) { - frame.destroy(); } -} -void dmabuf_t::frame( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t width, std::uint32_t height, - std::uint32_t x, std::uint32_t y, - std::uint32_t buffer_flags, std::uint32_t flags, - std::uint32_t format, - std::uint32_t high, std::uint32_t low, - std::uint32_t obj_count) { - auto next_frame = get_next_frame(); + void + dmabuf_t::listen(zwlr_export_dmabuf_manager_v1 *dmabuf_manager, wl_output *output, bool blend_cursor) { + auto frame = zwlr_export_dmabuf_manager_v1_capture_output(dmabuf_manager, blend_cursor, output); + zwlr_export_dmabuf_frame_v1_add_listener(frame, &listener, this); - next_frame->sd.fourcc = format; - next_frame->sd.width = width; - next_frame->sd.height = height; - next_frame->sd.modifier = (((std::uint64_t)high) << 32) | low; -} + status = WAITING; + } -void dmabuf_t::object( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t index, - std::int32_t fd, - std::uint32_t size, - std::uint32_t offset, - std::uint32_t stride, - std::uint32_t plane_index) { - auto next_frame = get_next_frame(); - - next_frame->sd.fds[plane_index] = fd; - next_frame->sd.pitches[plane_index] = stride; - next_frame->sd.offsets[plane_index] = offset; -} - -void dmabuf_t::ready( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec) { - - zwlr_export_dmabuf_frame_v1_destroy(frame); - - current_frame->destroy(); - current_frame = get_next_frame(); - - status = READY; -} - -void dmabuf_t::cancel( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t reason) { - - zwlr_export_dmabuf_frame_v1_destroy(frame); - - auto next_frame = get_next_frame(); - next_frame->destroy(); - - status = REINIT; -} - -void frame_t::destroy() { - for(auto x = 0; x < 4; ++x) { - if(sd.fds[x] >= 0) { - close(sd.fds[x]); - - sd.fds[x] = -1; + dmabuf_t::~dmabuf_t() { + for (auto &frame : frames) { + frame.destroy(); } } -} -frame_t::frame_t() { - // File descriptors aren't open - std::fill_n(sd.fds, 4, -1); -}; + void + dmabuf_t::frame( + zwlr_export_dmabuf_frame_v1 *frame, + std::uint32_t width, std::uint32_t height, + std::uint32_t x, std::uint32_t y, + std::uint32_t buffer_flags, std::uint32_t flags, + std::uint32_t format, + std::uint32_t high, std::uint32_t low, + std::uint32_t obj_count) { + auto next_frame = get_next_frame(); -std::vector> monitors(const char *display_name) { - display_t display; - - if(display.init(display_name)) { - return {}; + next_frame->sd.fourcc = format; + next_frame->sd.width = width; + next_frame->sd.height = height; + next_frame->sd.modifier = (((std::uint64_t) high) << 32) | low; } - interface_t interface; - interface.listen(display.registry()); + void + dmabuf_t::object( + zwlr_export_dmabuf_frame_v1 *frame, + std::uint32_t index, + std::int32_t fd, + std::uint32_t size, + std::uint32_t offset, + std::uint32_t stride, + std::uint32_t plane_index) { + auto next_frame = get_next_frame(); - display.roundtrip(); - - if(!interface[interface_t::XDG_OUTPUT]) { - BOOST_LOG(error) << "Missing Wayland wire XDG_OUTPUT"sv; - return {}; + next_frame->sd.fds[plane_index] = fd; + next_frame->sd.pitches[plane_index] = stride; + next_frame->sd.offsets[plane_index] = offset; } - for(auto &monitor : interface.monitors) { - monitor->listen(interface.output_manager); + void + dmabuf_t::ready( + zwlr_export_dmabuf_frame_v1 *frame, + std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec) { + zwlr_export_dmabuf_frame_v1_destroy(frame); + + current_frame->destroy(); + current_frame = get_next_frame(); + + status = READY; } - display.roundtrip(); + void + dmabuf_t::cancel( + zwlr_export_dmabuf_frame_v1 *frame, + std::uint32_t reason) { + zwlr_export_dmabuf_frame_v1_destroy(frame); - return std::move(interface.monitors); -} + auto next_frame = get_next_frame(); + next_frame->destroy(); -static bool validate() { - display_t display; + status = REINIT; + } - return display.init() == 0; -} + void + frame_t::destroy() { + for (auto x = 0; x < 4; ++x) { + if (sd.fds[x] >= 0) { + close(sd.fds[x]); -int init() { - static bool validated = validate(); + sd.fds[x] = -1; + } + } + } - return !validated; -} + frame_t::frame_t() { + // File descriptors aren't open + std::fill_n(sd.fds, 4, -1); + }; -} // namespace wl + std::vector> + monitors(const char *display_name) { + display_t display; + + if (display.init(display_name)) { + return {}; + } + + interface_t interface; + interface.listen(display.registry()); + + display.roundtrip(); + + if (!interface[interface_t::XDG_OUTPUT]) { + BOOST_LOG(error) << "Missing Wayland wire XDG_OUTPUT"sv; + return {}; + } + + for (auto &monitor : interface.monitors) { + monitor->listen(interface.output_manager); + } + + display.roundtrip(); + + return std::move(interface.monitors); + } + + static bool + validate() { + display_t display; + + return display.init() == 0; + } + + int + init() { + static bool validated = validate(); + + return !validated; + } + +} // namespace wl #pragma GCC diagnostic pop \ No newline at end of file diff --git a/src/platform/linux/wayland.h b/src/platform/linux/wayland.h index caf7e076..18caf990 100644 --- a/src/platform/linux/wayland.h +++ b/src/platform/linux/wayland.h @@ -4,8 +4,8 @@ #include #ifdef SUNSHINE_BUILD_WAYLAND -#include -#include + #include + #include #endif #include "graphics.h" @@ -17,200 +17,234 @@ #ifdef SUNSHINE_BUILD_WAYLAND namespace wl { -using display_internal_t = util::safe_ptr; + using display_internal_t = util::safe_ptr; -class frame_t { -public: - frame_t(); - egl::surface_descriptor_t sd; + class frame_t { + public: + frame_t(); + egl::surface_descriptor_t sd; - void destroy(); -}; - -class dmabuf_t { -public: - enum status_e { - WAITING, - READY, - REINIT, + void + destroy(); }; - dmabuf_t(dmabuf_t &&) = delete; - dmabuf_t(const dmabuf_t &) = delete; + class dmabuf_t { + public: + enum status_e { + WAITING, + READY, + REINIT, + }; - dmabuf_t &operator=(const dmabuf_t &) = delete; - dmabuf_t &operator=(dmabuf_t &&) = delete; + dmabuf_t(dmabuf_t &&) = delete; + dmabuf_t(const dmabuf_t &) = delete; - dmabuf_t(); + dmabuf_t & + operator=(const dmabuf_t &) = delete; + dmabuf_t & + operator=(dmabuf_t &&) = delete; - void listen(zwlr_export_dmabuf_manager_v1 *dmabuf_manager, wl_output *output, bool blend_cursor = false); + dmabuf_t(); - ~dmabuf_t(); + void + listen(zwlr_export_dmabuf_manager_v1 *dmabuf_manager, wl_output *output, bool blend_cursor = false); - void frame( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t width, std::uint32_t height, - std::uint32_t x, std::uint32_t y, - std::uint32_t buffer_flags, std::uint32_t flags, - std::uint32_t format, - std::uint32_t high, std::uint32_t low, - std::uint32_t obj_count); + ~dmabuf_t(); - void object( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t index, - std::int32_t fd, - std::uint32_t size, - std::uint32_t offset, - std::uint32_t stride, - std::uint32_t plane_index); + void + frame( + zwlr_export_dmabuf_frame_v1 *frame, + std::uint32_t width, std::uint32_t height, + std::uint32_t x, std::uint32_t y, + std::uint32_t buffer_flags, std::uint32_t flags, + std::uint32_t format, + std::uint32_t high, std::uint32_t low, + std::uint32_t obj_count); - void ready( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec); + void + object( + zwlr_export_dmabuf_frame_v1 *frame, + std::uint32_t index, + std::int32_t fd, + std::uint32_t size, + std::uint32_t offset, + std::uint32_t stride, + std::uint32_t plane_index); - void cancel( - zwlr_export_dmabuf_frame_v1 *frame, - std::uint32_t reason); + void + ready( + zwlr_export_dmabuf_frame_v1 *frame, + std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec); - inline frame_t *get_next_frame() { - return current_frame == &frames[0] ? &frames[1] : &frames[0]; - } + void + cancel( + zwlr_export_dmabuf_frame_v1 *frame, + std::uint32_t reason); - status_e status; + inline frame_t * + get_next_frame() { + return current_frame == &frames[0] ? &frames[1] : &frames[0]; + } - std::array frames; - frame_t *current_frame; + status_e status; - zwlr_export_dmabuf_frame_v1_listener listener; -}; + std::array frames; + frame_t *current_frame; -class monitor_t { -public: - monitor_t(monitor_t &&) = delete; - monitor_t(const monitor_t &) = delete; - - monitor_t &operator=(const monitor_t &) = delete; - monitor_t &operator=(monitor_t &&) = delete; - - monitor_t(wl_output *output); - - void xdg_name(zxdg_output_v1 *, const char *name); - void xdg_description(zxdg_output_v1 *, const char *description); - void xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y); - void xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height); - void xdg_done(zxdg_output_v1 *); - - void listen(zxdg_output_manager_v1 *output_manager); - - wl_output *output; - - std::string name; - std::string description; - - platf::touch_port_t viewport; - - zxdg_output_v1_listener listener; -}; - -class interface_t { - struct bind_t { - std::uint32_t id; - std::uint32_t version; + zwlr_export_dmabuf_frame_v1_listener listener; }; -public: - enum interface_e { - XDG_OUTPUT, - WLR_EXPORT_DMABUF, - MAX_INTERFACES, + class monitor_t { + public: + monitor_t(monitor_t &&) = delete; + monitor_t(const monitor_t &) = delete; + + monitor_t & + operator=(const monitor_t &) = delete; + monitor_t & + operator=(monitor_t &&) = delete; + + monitor_t(wl_output *output); + + void + xdg_name(zxdg_output_v1 *, const char *name); + void + xdg_description(zxdg_output_v1 *, const char *description); + void + xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y); + void + xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height); + void + xdg_done(zxdg_output_v1 *); + + void + listen(zxdg_output_manager_v1 *output_manager); + + wl_output *output; + + std::string name; + std::string description; + + platf::touch_port_t viewport; + + zxdg_output_v1_listener listener; }; - interface_t(interface_t &&) = delete; - interface_t(const interface_t &) = delete; + class interface_t { + struct bind_t { + std::uint32_t id; + std::uint32_t version; + }; - interface_t &operator=(const interface_t &) = delete; - interface_t &operator=(interface_t &&) = delete; + public: + enum interface_e { + XDG_OUTPUT, + WLR_EXPORT_DMABUF, + MAX_INTERFACES, + }; - interface_t() noexcept; + interface_t(interface_t &&) = delete; + interface_t(const interface_t &) = delete; - void listen(wl_registry *registry); + interface_t & + operator=(const interface_t &) = delete; + interface_t & + operator=(interface_t &&) = delete; - std::vector> monitors; + interface_t() noexcept; - zwlr_export_dmabuf_manager_v1 *dmabuf_manager; - zxdg_output_manager_v1 *output_manager; + void + listen(wl_registry *registry); - bool operator[](interface_e bit) const { - return interface[bit]; - } + std::vector> monitors; -private: - void add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version); - void del_interface(wl_registry *registry, uint32_t id); + zwlr_export_dmabuf_manager_v1 *dmabuf_manager; + zxdg_output_manager_v1 *output_manager; - std::bitset interface; + bool + operator[](interface_e bit) const { + return interface[bit]; + } - wl_registry_listener listener; -}; + private: + void + add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version); + void + del_interface(wl_registry *registry, uint32_t id); -class display_t { -public: - /** + std::bitset interface; + + wl_registry_listener listener; + }; + + class display_t { + public: + /** * Initialize display with display_name * If display_name == nullptr -> display_name = std::getenv("WAYLAND_DISPLAY") */ - int init(const char *display_name = nullptr); + int + init(const char *display_name = nullptr); - // Roundtrip with Wayland connection - void roundtrip(); + // Roundtrip with Wayland connection + void + roundtrip(); - // Get the registry associated with the display - // No need to manually free the registry - wl_registry *registry(); + // Get the registry associated with the display + // No need to manually free the registry + wl_registry * + registry(); - inline display_internal_t::pointer get() { - return display_internal.get(); - } + inline display_internal_t::pointer + get() { + return display_internal.get(); + } -private: - display_internal_t display_internal; -}; + private: + display_internal_t display_internal; + }; -std::vector> monitors(const char *display_name = nullptr); + std::vector> + monitors(const char *display_name = nullptr); -int init(); -} // namespace wl + int + init(); +} // namespace wl #else struct wl_output; struct zxdg_output_manager_v1; namespace wl { -class monitor_t { -public: - monitor_t(monitor_t &&) = delete; - monitor_t(const monitor_t &) = delete; + class monitor_t { + public: + monitor_t(monitor_t &&) = delete; + monitor_t(const monitor_t &) = delete; - monitor_t &operator=(const monitor_t &) = delete; - monitor_t &operator=(monitor_t &&) = delete; + monitor_t & + operator=(const monitor_t &) = delete; + monitor_t & + operator=(monitor_t &&) = delete; - monitor_t(wl_output *output); + monitor_t(wl_output *output); - void listen(zxdg_output_manager_v1 *output_manager); + void + listen(zxdg_output_manager_v1 *output_manager); - wl_output *output; + wl_output *output; - std::string name; - std::string description; + std::string name; + std::string description; - platf::touch_port_t viewport; -}; + platf::touch_port_t viewport; + }; -inline std::vector> monitors(const char *display_name = nullptr) { return {}; } + inline std::vector> + monitors(const char *display_name = nullptr) { return {}; } -inline int init() { return -1; } -} // namespace wl + inline int + init() { return -1; } +} // namespace wl #endif #endif \ No newline at end of file diff --git a/src/platform/linux/wlgrab.cpp b/src/platform/linux/wlgrab.cpp index 00a34c28..db4d24a5 100644 --- a/src/platform/linux/wlgrab.cpp +++ b/src/platform/linux/wlgrab.cpp @@ -7,372 +7,386 @@ using namespace std::literals; namespace wl { -static int env_width; -static int env_height; + static int env_width; + static int env_height; -struct img_t : public platf::img_t { - ~img_t() override { - delete[] data; - data = nullptr; - } -}; - -class wlr_t : public platf::display_t { -public: - int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { - delay = std::chrono::nanoseconds { 1s } / config.framerate; - mem_type = hwdevice_type; - - if(display.init()) { - return -1; + struct img_t: public platf::img_t { + ~img_t() override { + delete[] data; + data = nullptr; } + }; - interface.listen(display.registry()); + class wlr_t: public platf::display_t { + public: + int + init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { + delay = std::chrono::nanoseconds { 1s } / config.framerate; + mem_type = hwdevice_type; - display.roundtrip(); - - if(!interface[wl::interface_t::XDG_OUTPUT]) { - BOOST_LOG(error) << "Missing Wayland wire for xdg_output"sv; - return -1; - } - - if(!interface[wl::interface_t::WLR_EXPORT_DMABUF]) { - BOOST_LOG(error) << "Missing Wayland wire for wlr-export-dmabuf"sv; - return -1; - } - - auto monitor = interface.monitors[0].get(); - - if(!display_name.empty()) { - auto streamedMonitor = util::from_view(display_name); - - if(streamedMonitor >= 0 && streamedMonitor < interface.monitors.size()) { - monitor = interface.monitors[streamedMonitor].get(); + if (display.init()) { + return -1; } - } - monitor->listen(interface.output_manager); + interface.listen(display.registry()); - display.roundtrip(); - - output = monitor->output; - - offset_x = monitor->viewport.offset_x; - offset_y = monitor->viewport.offset_y; - width = monitor->viewport.width; - height = monitor->viewport.height; - - this->env_width = ::wl::env_width; - this->env_height = ::wl::env_height; - - BOOST_LOG(info) << "Selected monitor ["sv << monitor->description << "] for streaming"sv; - BOOST_LOG(debug) << "Offset: "sv << offset_x << 'x' << offset_y; - BOOST_LOG(debug) << "Resolution: "sv << width << 'x' << height; - BOOST_LOG(debug) << "Desktop Resolution: "sv << env_width << 'x' << env_height; - - return 0; - } - - int dummy_img(platf::img_t *img) override { - return 0; - } - - inline platf::capture_e snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { - auto to = std::chrono::steady_clock::now() + timeout; - - dmabuf.listen(interface.dmabuf_manager, output, cursor); - do { display.roundtrip(); - if(to < std::chrono::steady_clock::now()) { - return platf::capture_e::timeout; + if (!interface[wl::interface_t::XDG_OUTPUT]) { + BOOST_LOG(error) << "Missing Wayland wire for xdg_output"sv; + return -1; } - } while(dmabuf.status == dmabuf_t::WAITING); - auto current_frame = dmabuf.current_frame; + if (!interface[wl::interface_t::WLR_EXPORT_DMABUF]) { + BOOST_LOG(error) << "Missing Wayland wire for wlr-export-dmabuf"sv; + return -1; + } - if( - dmabuf.status == dmabuf_t::REINIT || - current_frame->sd.width != width || - current_frame->sd.height != height) { + auto monitor = interface.monitors[0].get(); - return platf::capture_e::reinit; + if (!display_name.empty()) { + auto streamedMonitor = util::from_view(display_name); + + if (streamedMonitor >= 0 && streamedMonitor < interface.monitors.size()) { + monitor = interface.monitors[streamedMonitor].get(); + } + } + + monitor->listen(interface.output_manager); + + display.roundtrip(); + + output = monitor->output; + + offset_x = monitor->viewport.offset_x; + offset_y = monitor->viewport.offset_y; + width = monitor->viewport.width; + height = monitor->viewport.height; + + this->env_width = ::wl::env_width; + this->env_height = ::wl::env_height; + + BOOST_LOG(info) << "Selected monitor ["sv << monitor->description << "] for streaming"sv; + BOOST_LOG(debug) << "Offset: "sv << offset_x << 'x' << offset_y; + BOOST_LOG(debug) << "Resolution: "sv << width << 'x' << height; + BOOST_LOG(debug) << "Desktop Resolution: "sv << env_width << 'x' << env_height; + + return 0; } - return platf::capture_e::ok; - } + int + dummy_img(platf::img_t *img) override { + return 0; + } - platf::mem_type_e mem_type; + inline platf::capture_e + snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { + auto to = std::chrono::steady_clock::now() + timeout; - std::chrono::nanoseconds delay; + dmabuf.listen(interface.dmabuf_manager, output, cursor); + do { + display.roundtrip(); - wl::display_t display; - interface_t interface; - dmabuf_t dmabuf; + if (to < std::chrono::steady_clock::now()) { + return platf::capture_e::timeout; + } + } while (dmabuf.status == dmabuf_t::WAITING); - wl_output *output; -}; + auto current_frame = dmabuf.current_frame; -class wlr_ram_t : public wlr_t { -public: - platf::capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { - auto next_frame = std::chrono::steady_clock::now(); - - while(img) { - auto now = std::chrono::steady_clock::now(); - - if(next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + if ( + dmabuf.status == dmabuf_t::REINIT || + current_frame->sd.width != width || + current_frame->sd.height != height) { + return platf::capture_e::reinit; } - while(next_frame > now) { - now = std::chrono::steady_clock::now(); - } - next_frame = now + delay; - auto status = snapshot(img.get(), 1000ms, *cursor); - switch(status) { - case platf::capture_e::reinit: - case platf::capture_e::error: - return status; - case platf::capture_e::timeout: - img = snapshot_cb(img, false); - break; - case platf::capture_e::ok: - img = snapshot_cb(img, true); - break; - default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']'; + return platf::capture_e::ok; + } + + platf::mem_type_e mem_type; + + std::chrono::nanoseconds delay; + + wl::display_t display; + interface_t interface; + dmabuf_t dmabuf; + + wl_output *output; + }; + + class wlr_ram_t: public wlr_t { + public: + platf::capture_e + capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { + auto next_frame = std::chrono::steady_clock::now(); + + while (img) { + auto now = std::chrono::steady_clock::now(); + + if (next_frame > now) { + std::this_thread::sleep_for((next_frame - now) / 3 * 2); + } + while (next_frame > now) { + now = std::chrono::steady_clock::now(); + } + next_frame = now + delay; + + auto status = snapshot(img.get(), 1000ms, *cursor); + switch (status) { + case platf::capture_e::reinit: + case platf::capture_e::error: + return status; + case platf::capture_e::timeout: + img = snapshot_cb(img, false); + break; + case platf::capture_e::ok: + img = snapshot_cb(img, true); + break; + default: + BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; + return status; + } + } + + return platf::capture_e::ok; + } + + platf::capture_e + snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { + auto status = wlr_t::snapshot(img_out_base, timeout, cursor); + if (status != platf::capture_e::ok) { return status; } - } - return platf::capture_e::ok; - } + auto current_frame = dmabuf.current_frame; - platf::capture_e snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { - auto status = wlr_t::snapshot(img_out_base, timeout, cursor); - if(status != platf::capture_e::ok) { - return status; - } + auto rgb_opt = egl::import_source(egl_display.get(), current_frame->sd); - auto current_frame = dmabuf.current_frame; - - auto rgb_opt = egl::import_source(egl_display.get(), current_frame->sd); - - if(!rgb_opt) { - return platf::capture_e::reinit; - } - - gl::ctx.BindTexture(GL_TEXTURE_2D, (*rgb_opt)->tex[0]); - - int w, h; - gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); - gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); - BOOST_LOG(debug) << "width and height: w "sv << w << " h "sv << h; - - gl::ctx.GetTextureSubImage((*rgb_opt)->tex[0], 0, 0, 0, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out_base->height * img_out_base->row_pitch, img_out_base->data); - gl::ctx.BindTexture(GL_TEXTURE_2D, 0); - - return platf::capture_e::ok; - } - - int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { - if(wlr_t::init(hwdevice_type, display_name, config)) { - return -1; - } - - egl_display = egl::make_display(display.get()); - if(!egl_display) { - return -1; - } - - auto ctx_opt = egl::make_ctx(egl_display.get()); - if(!ctx_opt) { - return -1; - } - - ctx = std::move(*ctx_opt); - - return 0; - } - - std::shared_ptr make_hwdevice(platf::pix_fmt_e pix_fmt) override { - if(mem_type == platf::mem_type_e::vaapi) { - return va::make_hwdevice(width, height, false); - } - - return std::make_shared(); - } - - std::shared_ptr alloc_img() override { - auto img = std::make_shared(); - img->width = width; - img->height = height; - img->pixel_pitch = 4; - img->row_pitch = img->pixel_pitch * width; - img->data = new std::uint8_t[height * img->row_pitch]; - - return img; - } - - egl::display_t egl_display; - egl::ctx_t ctx; -}; - -class wlr_vram_t : public wlr_t { -public: - platf::capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { - auto next_frame = std::chrono::steady_clock::now(); - - while(img) { - auto now = std::chrono::steady_clock::now(); - - if(next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); + if (!rgb_opt) { + return platf::capture_e::reinit; } - while(next_frame > now) { - now = std::chrono::steady_clock::now(); - } - next_frame = now + delay; - auto status = snapshot(img.get(), 1000ms, *cursor); - switch(status) { - case platf::capture_e::reinit: - case platf::capture_e::error: - return status; - case platf::capture_e::timeout: - img = snapshot_cb(img, false); - break; - case platf::capture_e::ok: - img = snapshot_cb(img, true); - break; - default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']'; + gl::ctx.BindTexture(GL_TEXTURE_2D, (*rgb_opt)->tex[0]); + + int w, h; + gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); + gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); + BOOST_LOG(debug) << "width and height: w "sv << w << " h "sv << h; + + gl::ctx.GetTextureSubImage((*rgb_opt)->tex[0], 0, 0, 0, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out_base->height * img_out_base->row_pitch, img_out_base->data); + gl::ctx.BindTexture(GL_TEXTURE_2D, 0); + + return platf::capture_e::ok; + } + + int + init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { + if (wlr_t::init(hwdevice_type, display_name, config)) { + return -1; + } + + egl_display = egl::make_display(display.get()); + if (!egl_display) { + return -1; + } + + auto ctx_opt = egl::make_ctx(egl_display.get()); + if (!ctx_opt) { + return -1; + } + + ctx = std::move(*ctx_opt); + + return 0; + } + + std::shared_ptr + make_hwdevice(platf::pix_fmt_e pix_fmt) override { + if (mem_type == platf::mem_type_e::vaapi) { + return va::make_hwdevice(width, height, false); + } + + return std::make_shared(); + } + + std::shared_ptr + alloc_img() override { + auto img = std::make_shared(); + img->width = width; + img->height = height; + img->pixel_pitch = 4; + img->row_pitch = img->pixel_pitch * width; + img->data = new std::uint8_t[height * img->row_pitch]; + + return img; + } + + egl::display_t egl_display; + egl::ctx_t ctx; + }; + + class wlr_vram_t: public wlr_t { + public: + platf::capture_e + capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { + auto next_frame = std::chrono::steady_clock::now(); + + while (img) { + auto now = std::chrono::steady_clock::now(); + + if (next_frame > now) { + std::this_thread::sleep_for((next_frame - now) / 3 * 2); + } + while (next_frame > now) { + now = std::chrono::steady_clock::now(); + } + next_frame = now + delay; + + auto status = snapshot(img.get(), 1000ms, *cursor); + switch (status) { + case platf::capture_e::reinit: + case platf::capture_e::error: + return status; + case platf::capture_e::timeout: + img = snapshot_cb(img, false); + break; + case platf::capture_e::ok: + img = snapshot_cb(img, true); + break; + default: + BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; + return status; + } + } + + return platf::capture_e::ok; + } + + platf::capture_e + snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { + auto status = wlr_t::snapshot(img_out_base, timeout, cursor); + if (status != platf::capture_e::ok) { return status; } + + auto img = (egl::img_descriptor_t *) img_out_base; + img->reset(); + + auto current_frame = dmabuf.current_frame; + + ++sequence; + img->sequence = sequence; + + img->sd = current_frame->sd; + + // Prevent dmabuf from closing the file descriptors. + std::fill_n(current_frame->sd.fds, 4, -1); + + return platf::capture_e::ok; } - return platf::capture_e::ok; - } + std::shared_ptr + alloc_img() override { + auto img = std::make_shared(); - platf::capture_e snapshot(platf::img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { - auto status = wlr_t::snapshot(img_out_base, timeout, cursor); - if(status != platf::capture_e::ok) { - return status; + img->sequence = 0; + img->serial = std::numeric_limitsserial)>::max(); + img->data = nullptr; + + // File descriptors aren't open + std::fill_n(img->sd.fds, 4, -1); + + return img; } - auto img = (egl::img_descriptor_t *)img_out_base; - img->reset(); + std::shared_ptr + make_hwdevice(platf::pix_fmt_e pix_fmt) override { + if (mem_type == platf::mem_type_e::vaapi) { + return va::make_hwdevice(width, height, 0, 0, true); + } - auto current_frame = dmabuf.current_frame; - - ++sequence; - img->sequence = sequence; - - img->sd = current_frame->sd; - - // Prevent dmabuf from closing the file descriptors. - std::fill_n(current_frame->sd.fds, 4, -1); - - return platf::capture_e::ok; - } - - std::shared_ptr alloc_img() override { - auto img = std::make_shared(); - - img->sequence = 0; - img->serial = std::numeric_limitsserial)>::max(); - img->data = nullptr; - - // File descriptors aren't open - std::fill_n(img->sd.fds, 4, -1); - - return img; - } - - std::shared_ptr make_hwdevice(platf::pix_fmt_e pix_fmt) override { - if(mem_type == platf::mem_type_e::vaapi) { - return va::make_hwdevice(width, height, 0, 0, true); + return std::make_shared(); } - return std::make_shared(); - } + int + dummy_img(platf::img_t *img) override { + return snapshot(img, 1000ms, false) != platf::capture_e::ok; + } - int dummy_img(platf::img_t *img) override { - return snapshot(img, 1000ms, false) != platf::capture_e::ok; - } + std::uint64_t sequence {}; + }; - std::uint64_t sequence {}; -}; - -} // namespace wl +} // namespace wl namespace platf { -std::shared_ptr wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { - if(hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) { - BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; - return nullptr; - } + std::shared_ptr + wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { + if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) { + BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; + return nullptr; + } - if(hwdevice_type == platf::mem_type_e::vaapi) { - auto wlr = std::make_shared(); - if(wlr->init(hwdevice_type, display_name, config)) { + if (hwdevice_type == platf::mem_type_e::vaapi) { + auto wlr = std::make_shared(); + if (wlr->init(hwdevice_type, display_name, config)) { + return nullptr; + } + + return wlr; + } + + auto wlr = std::make_shared(); + if (wlr->init(hwdevice_type, display_name, config)) { return nullptr; } return wlr; } - auto wlr = std::make_shared(); - if(wlr->init(hwdevice_type, display_name, config)) { - return nullptr; + std::vector + wl_display_names() { + std::vector display_names; + + wl::display_t display; + if (display.init()) { + return {}; + } + + wl::interface_t interface; + interface.listen(display.registry()); + + display.roundtrip(); + + if (!interface[wl::interface_t::XDG_OUTPUT]) { + BOOST_LOG(warning) << "Missing Wayland wire for xdg_output"sv; + return {}; + } + + if (!interface[wl::interface_t::WLR_EXPORT_DMABUF]) { + BOOST_LOG(warning) << "Missing Wayland wire for wlr-export-dmabuf"sv; + return {}; + } + + wl::env_width = 0; + wl::env_height = 0; + + for (auto &monitor : interface.monitors) { + monitor->listen(interface.output_manager); + } + + display.roundtrip(); + + for (int x = 0; x < interface.monitors.size(); ++x) { + auto monitor = interface.monitors[x].get(); + + wl::env_width = std::max(wl::env_width, (int) (monitor->viewport.offset_x + monitor->viewport.width)); + wl::env_height = std::max(wl::env_height, (int) (monitor->viewport.offset_y + monitor->viewport.height)); + + display_names.emplace_back(std::to_string(x)); + } + + return display_names; } - return wlr; -} - -std::vector wl_display_names() { - std::vector display_names; - - wl::display_t display; - if(display.init()) { - return {}; - } - - wl::interface_t interface; - interface.listen(display.registry()); - - display.roundtrip(); - - if(!interface[wl::interface_t::XDG_OUTPUT]) { - BOOST_LOG(warning) << "Missing Wayland wire for xdg_output"sv; - return {}; - } - - if(!interface[wl::interface_t::WLR_EXPORT_DMABUF]) { - BOOST_LOG(warning) << "Missing Wayland wire for wlr-export-dmabuf"sv; - return {}; - } - - wl::env_width = 0; - wl::env_height = 0; - - for(auto &monitor : interface.monitors) { - monitor->listen(interface.output_manager); - } - - display.roundtrip(); - - for(int x = 0; x < interface.monitors.size(); ++x) { - auto monitor = interface.monitors[x].get(); - - wl::env_width = std::max(wl::env_width, (int)(monitor->viewport.offset_x + monitor->viewport.width)); - wl::env_height = std::max(wl::env_height, (int)(monitor->viewport.offset_y + monitor->viewport.height)); - - display_names.emplace_back(std::to_string(x)); - } - - return display_names; -} - -} // namespace platf +} // namespace platf diff --git a/src/platform/linux/x11grab.cpp b/src/platform/linux/x11grab.cpp index db7b05b6..7aab70ee 100644 --- a/src/platform/linux/x11grab.cpp +++ b/src/platform/linux/x11grab.cpp @@ -30,809 +30,853 @@ using namespace std::literals; namespace platf { -int load_xcb(); -int load_x11(); + int + load_xcb(); + int + load_x11(); -namespace x11 { + namespace x11 { #define _FN(x, ret, args) \ typedef ret(*x##_fn) args; \ static x##_fn x -_FN(GetImage, XImage *, - ( - Display * display, - Drawable d, - int x, int y, - unsigned int width, unsigned int height, - unsigned long plane_mask, - int format)); - -_FN(OpenDisplay, Display *, (_Xconst char *display_name)); -_FN(GetWindowAttributes, Status, - ( - Display * display, - Window w, - XWindowAttributes *window_attributes_return)); - -_FN(CloseDisplay, int, (Display * display)); -_FN(Free, int, (void *data)); -_FN(InitThreads, Status, (void)); - -namespace rr { -_FN(GetScreenResources, XRRScreenResources *, (Display * dpy, Window window)); -_FN(GetOutputInfo, XRROutputInfo *, (Display * dpy, XRRScreenResources *resources, RROutput output)); -_FN(GetCrtcInfo, XRRCrtcInfo *, (Display * dpy, XRRScreenResources *resources, RRCrtc crtc)); -_FN(FreeScreenResources, void, (XRRScreenResources * resources)); -_FN(FreeOutputInfo, void, (XRROutputInfo * outputInfo)); -_FN(FreeCrtcInfo, void, (XRRCrtcInfo * crtcInfo)); - -static int init() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if(funcs_loaded) return 0; - - if(!handle) { - handle = dyn::handle({ "libXrandr.so.2", "libXrandr.so" }); - if(!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *)&GetScreenResources, "XRRGetScreenResources" }, - { (dyn::apiproc *)&GetOutputInfo, "XRRGetOutputInfo" }, - { (dyn::apiproc *)&GetCrtcInfo, "XRRGetCrtcInfo" }, - { (dyn::apiproc *)&FreeScreenResources, "XRRFreeScreenResources" }, - { (dyn::apiproc *)&FreeOutputInfo, "XRRFreeOutputInfo" }, - { (dyn::apiproc *)&FreeCrtcInfo, "XRRFreeCrtcInfo" }, - }; - - if(dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; -} - -} // namespace rr -namespace fix { -_FN(GetCursorImage, XFixesCursorImage *, (Display * dpy)); - -static int init() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if(funcs_loaded) return 0; - - if(!handle) { - handle = dyn::handle({ "libXfixes.so.3", "libXfixes.so" }); - if(!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *)&GetCursorImage, "XFixesGetCursorImage" }, - }; - - if(dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; -} -} // namespace fix - -static int init() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if(funcs_loaded) return 0; - - if(!handle) { - handle = dyn::handle({ "libX11.so.6", "libX11.so" }); - if(!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *)&GetImage, "XGetImage" }, - { (dyn::apiproc *)&OpenDisplay, "XOpenDisplay" }, - { (dyn::apiproc *)&GetWindowAttributes, "XGetWindowAttributes" }, - { (dyn::apiproc *)&Free, "XFree" }, - { (dyn::apiproc *)&CloseDisplay, "XCloseDisplay" }, - { (dyn::apiproc *)&InitThreads, "XInitThreads" }, - }; - - if(dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; -} -} // namespace x11 - -namespace xcb { -static xcb_extension_t *shm_id; - -_FN(shm_get_image_reply, xcb_shm_get_image_reply_t *, - ( - xcb_connection_t * c, - xcb_shm_get_image_cookie_t cookie, - xcb_generic_error_t **e)); - -_FN(shm_get_image_unchecked, xcb_shm_get_image_cookie_t, - ( - xcb_connection_t * c, - xcb_drawable_t drawable, - int16_t x, int16_t y, - uint16_t width, uint16_t height, - uint32_t plane_mask, - uint8_t format, - xcb_shm_seg_t shmseg, - uint32_t offset)); - -_FN(shm_attach, xcb_void_cookie_t, - (xcb_connection_t * c, - xcb_shm_seg_t shmseg, - uint32_t shmid, - uint8_t read_only)); - -_FN(get_extension_data, xcb_query_extension_reply_t *, - (xcb_connection_t * c, xcb_extension_t *ext)); - -_FN(get_setup, xcb_setup_t *, (xcb_connection_t * c)); -_FN(disconnect, void, (xcb_connection_t * c)); -_FN(connection_has_error, int, (xcb_connection_t * c)); -_FN(connect, xcb_connection_t *, (const char *displayname, int *screenp)); -_FN(setup_roots_iterator, xcb_screen_iterator_t, (const xcb_setup_t *R)); -_FN(generate_id, std::uint32_t, (xcb_connection_t * c)); - -int init_shm() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if(funcs_loaded) return 0; - - if(!handle) { - handle = dyn::handle({ "libxcb-shm.so.0", "libxcb-shm.so" }); - if(!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *)&shm_id, "xcb_shm_id" }, - { (dyn::apiproc *)&shm_get_image_reply, "xcb_shm_get_image_reply" }, - { (dyn::apiproc *)&shm_get_image_unchecked, "xcb_shm_get_image_unchecked" }, - { (dyn::apiproc *)&shm_attach, "xcb_shm_attach" }, - }; - - if(dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; -} - -int init() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if(funcs_loaded) return 0; - - if(!handle) { - handle = dyn::handle({ "libxcb.so.1", "libxcb.so" }); - if(!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *)&get_extension_data, "xcb_get_extension_data" }, - { (dyn::apiproc *)&get_setup, "xcb_get_setup" }, - { (dyn::apiproc *)&disconnect, "xcb_disconnect" }, - { (dyn::apiproc *)&connection_has_error, "xcb_connection_has_error" }, - { (dyn::apiproc *)&connect, "xcb_connect" }, - { (dyn::apiproc *)&setup_roots_iterator, "xcb_setup_roots_iterator" }, - { (dyn::apiproc *)&generate_id, "xcb_generate_id" }, - }; - - if(dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; -} - -#undef _FN -} // namespace xcb - -void freeImage(XImage *); -void freeX(XFixesCursorImage *); - -using xcb_connect_t = util::dyn_safe_ptr; -using xcb_img_t = util::c_ptr; - -using ximg_t = util::safe_ptr; -using xcursor_t = util::safe_ptr; - -using crtc_info_t = util::dyn_safe_ptr<_XRRCrtcInfo, &x11::rr::FreeCrtcInfo>; -using output_info_t = util::dyn_safe_ptr<_XRROutputInfo, &x11::rr::FreeOutputInfo>; -using screen_res_t = util::dyn_safe_ptr<_XRRScreenResources, &x11::rr::FreeScreenResources>; - -class shm_id_t { -public: - shm_id_t() : id { -1 } {} - shm_id_t(int id) : id { id } {} - shm_id_t(shm_id_t &&other) noexcept : id(other.id) { - other.id = -1; - } - - ~shm_id_t() { - if(id != -1) { - shmctl(id, IPC_RMID, nullptr); - id = -1; - } - } - int id; -}; - -class shm_data_t { -public: - shm_data_t() : data { (void *)-1 } {} - shm_data_t(void *data) : data { data } {} - - shm_data_t(shm_data_t &&other) noexcept : data(other.data) { - other.data = (void *)-1; - } - - ~shm_data_t() { - if((std::uintptr_t)data != -1) { - shmdt(data); - } - } - - void *data; -}; - -struct x11_img_t : public img_t { - ximg_t img; -}; - -struct shm_img_t : public img_t { - ~shm_img_t() override { - delete[] data; - data = nullptr; - } -}; - -static void blend_cursor(Display *display, img_t &img, int offsetX, int offsetY) { - xcursor_t overlay { x11::fix::GetCursorImage(display) }; - - if(!overlay) { - BOOST_LOG(error) << "Couldn't get cursor from XFixesGetCursorImage"sv; - return; - } - - overlay->x -= overlay->xhot; - overlay->y -= overlay->yhot; - - overlay->x -= offsetX; - overlay->y -= offsetY; - - overlay->x = std::max((short)0, overlay->x); - overlay->y = std::max((short)0, overlay->y); - - auto pixels = (int *)img.data; - - auto screen_height = img.height; - auto screen_width = img.width; - - auto delta_height = std::min(overlay->height, std::max(0, screen_height - overlay->y)); - auto delta_width = std::min(overlay->width, std::max(0, screen_width - overlay->x)); - for(auto y = 0; y < delta_height; ++y) { - auto overlay_begin = &overlay->pixels[y * overlay->width]; - auto overlay_end = &overlay->pixels[y * overlay->width + delta_width]; - - auto pixels_begin = &pixels[(y + overlay->y) * (img.row_pitch / img.pixel_pitch) + overlay->x]; - - std::for_each(overlay_begin, overlay_end, [&](long pixel) { - int *pixel_p = (int *)&pixel; - - auto colors_in = (uint8_t *)pixels_begin; - - auto alpha = (*(uint *)pixel_p) >> 24u; - if(alpha == 255) { - *pixels_begin = *pixel_p; - } - else { - auto colors_out = (uint8_t *)pixel_p; - colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255; - colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255; - colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255; - } - ++pixels_begin; - }); - } -} - -struct x11_attr_t : public display_t { - std::chrono::nanoseconds delay; - - x11::xdisplay_t xdisplay; - Window xwindow; - XWindowAttributes xattr; - - mem_type_e mem_type; - - /* - * Last X (NOT the streamed monitor!) size. - * This way we can trigger reinitialization if the dimensions changed while streaming - */ - // int env_width, env_height; - - x11_attr_t(mem_type_e mem_type) : xdisplay { x11::OpenDisplay(nullptr) }, xwindow {}, xattr {}, mem_type { mem_type } { - x11::InitThreads(); - } - - int init(const std::string &display_name, const ::video::config_t &config) { - if(!xdisplay) { - BOOST_LOG(error) << "Could not open X11 display"sv; - return -1; - } - - delay = std::chrono::nanoseconds { 1s } / config.framerate; - - xwindow = DefaultRootWindow(xdisplay.get()); - - refresh(); - - int streamedMonitor = -1; - if(!display_name.empty()) { - streamedMonitor = (int)util::from_view(display_name); - } - - if(streamedMonitor != -1) { - BOOST_LOG(info) << "Configuring selected monitor ("sv << streamedMonitor << ") to stream"sv; - screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) }; - int output = screenr->noutput; - - output_info_t result; - int monitor = 0; - for(int x = 0; x < output; ++x) { - output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) }; - if(out_info && out_info->connection == RR_Connected) { - if(monitor++ == streamedMonitor) { - result = std::move(out_info); - break; + _FN(GetImage, XImage *, + ( + Display * display, + Drawable d, + int x, int y, + unsigned int width, unsigned int height, + unsigned long plane_mask, + int format)); + + _FN(OpenDisplay, Display *, (_Xconst char *display_name)); + _FN(GetWindowAttributes, Status, + ( + Display * display, + Window w, + XWindowAttributes *window_attributes_return)); + + _FN(CloseDisplay, int, (Display * display)); + _FN(Free, int, (void *data)); + _FN(InitThreads, Status, (void) ); + + namespace rr { + _FN(GetScreenResources, XRRScreenResources *, (Display * dpy, Window window)); + _FN(GetOutputInfo, XRROutputInfo *, (Display * dpy, XRRScreenResources *resources, RROutput output)); + _FN(GetCrtcInfo, XRRCrtcInfo *, (Display * dpy, XRRScreenResources *resources, RRCrtc crtc)); + _FN(FreeScreenResources, void, (XRRScreenResources * resources)); + _FN(FreeOutputInfo, void, (XRROutputInfo * outputInfo)); + _FN(FreeCrtcInfo, void, (XRRCrtcInfo * crtcInfo)); + + static int + init() { + static void *handle { nullptr }; + static bool funcs_loaded = false; + + if (funcs_loaded) return 0; + + if (!handle) { + handle = dyn::handle({ "libXrandr.so.2", "libXrandr.so" }); + if (!handle) { + return -1; } } + + std::vector> funcs { + { (dyn::apiproc *) &GetScreenResources, "XRRGetScreenResources" }, + { (dyn::apiproc *) &GetOutputInfo, "XRRGetOutputInfo" }, + { (dyn::apiproc *) &GetCrtcInfo, "XRRGetCrtcInfo" }, + { (dyn::apiproc *) &FreeScreenResources, "XRRFreeScreenResources" }, + { (dyn::apiproc *) &FreeOutputInfo, "XRRFreeOutputInfo" }, + { (dyn::apiproc *) &FreeCrtcInfo, "XRRFreeCrtcInfo" }, + }; + + if (dyn::load(handle, funcs)) { + return -1; + } + + funcs_loaded = true; + return 0; + } + + } // namespace rr + namespace fix { + _FN(GetCursorImage, XFixesCursorImage *, (Display * dpy)); + + static int + init() { + static void *handle { nullptr }; + static bool funcs_loaded = false; + + if (funcs_loaded) return 0; + + if (!handle) { + handle = dyn::handle({ "libXfixes.so.3", "libXfixes.so" }); + if (!handle) { + return -1; + } + } + + std::vector> funcs { + { (dyn::apiproc *) &GetCursorImage, "XFixesGetCursorImage" }, + }; + + if (dyn::load(handle, funcs)) { + return -1; + } + + funcs_loaded = true; + return 0; + } + } // namespace fix + + static int + init() { + static void *handle { nullptr }; + static bool funcs_loaded = false; + + if (funcs_loaded) return 0; + + if (!handle) { + handle = dyn::handle({ "libX11.so.6", "libX11.so" }); + if (!handle) { + return -1; + } } - if(!result) { - BOOST_LOG(error) << "Could not stream display number ["sv << streamedMonitor << "], there are only ["sv << monitor << "] displays."sv; + std::vector> funcs { + { (dyn::apiproc *) &GetImage, "XGetImage" }, + { (dyn::apiproc *) &OpenDisplay, "XOpenDisplay" }, + { (dyn::apiproc *) &GetWindowAttributes, "XGetWindowAttributes" }, + { (dyn::apiproc *) &Free, "XFree" }, + { (dyn::apiproc *) &CloseDisplay, "XCloseDisplay" }, + { (dyn::apiproc *) &InitThreads, "XInitThreads" }, + }; + + if (dyn::load(handle, funcs)) { return -1; } - if(result->crtc) { - crtc_info_t crt_info { x11::rr::GetCrtcInfo(xdisplay.get(), screenr.get(), result->crtc) }; - BOOST_LOG(info) - << "Streaming display: "sv << result->name << " with res "sv << crt_info->width << 'x' << crt_info->height << " offset by "sv << crt_info->x << 'x' << crt_info->y; + funcs_loaded = true; + return 0; + } + } // namespace x11 - width = crt_info->width; - height = crt_info->height; - offset_x = crt_info->x; - offset_y = crt_info->y; + namespace xcb { + static xcb_extension_t *shm_id; + + _FN(shm_get_image_reply, xcb_shm_get_image_reply_t *, + ( + xcb_connection_t * c, + xcb_shm_get_image_cookie_t cookie, + xcb_generic_error_t **e)); + + _FN(shm_get_image_unchecked, xcb_shm_get_image_cookie_t, + ( + xcb_connection_t * c, + xcb_drawable_t drawable, + int16_t x, int16_t y, + uint16_t width, uint16_t height, + uint32_t plane_mask, + uint8_t format, + xcb_shm_seg_t shmseg, + uint32_t offset)); + + _FN(shm_attach, xcb_void_cookie_t, + (xcb_connection_t * c, + xcb_shm_seg_t shmseg, + uint32_t shmid, + uint8_t read_only)); + + _FN(get_extension_data, xcb_query_extension_reply_t *, + (xcb_connection_t * c, xcb_extension_t *ext)); + + _FN(get_setup, xcb_setup_t *, (xcb_connection_t * c)); + _FN(disconnect, void, (xcb_connection_t * c)); + _FN(connection_has_error, int, (xcb_connection_t * c)); + _FN(connect, xcb_connection_t *, (const char *displayname, int *screenp)); + _FN(setup_roots_iterator, xcb_screen_iterator_t, (const xcb_setup_t *R)); + _FN(generate_id, std::uint32_t, (xcb_connection_t * c)); + + int + init_shm() { + static void *handle { nullptr }; + static bool funcs_loaded = false; + + if (funcs_loaded) return 0; + + if (!handle) { + handle = dyn::handle({ "libxcb-shm.so.0", "libxcb-shm.so" }); + if (!handle) { + return -1; + } + } + + std::vector> funcs { + { (dyn::apiproc *) &shm_id, "xcb_shm_id" }, + { (dyn::apiproc *) &shm_get_image_reply, "xcb_shm_get_image_reply" }, + { (dyn::apiproc *) &shm_get_image_unchecked, "xcb_shm_get_image_unchecked" }, + { (dyn::apiproc *) &shm_attach, "xcb_shm_attach" }, + }; + + if (dyn::load(handle, funcs)) { + return -1; + } + + funcs_loaded = true; + return 0; + } + + int + init() { + static void *handle { nullptr }; + static bool funcs_loaded = false; + + if (funcs_loaded) return 0; + + if (!handle) { + handle = dyn::handle({ "libxcb.so.1", "libxcb.so" }); + if (!handle) { + return -1; + } + } + + std::vector> funcs { + { (dyn::apiproc *) &get_extension_data, "xcb_get_extension_data" }, + { (dyn::apiproc *) &get_setup, "xcb_get_setup" }, + { (dyn::apiproc *) &disconnect, "xcb_disconnect" }, + { (dyn::apiproc *) &connection_has_error, "xcb_connection_has_error" }, + { (dyn::apiproc *) &connect, "xcb_connect" }, + { (dyn::apiproc *) &setup_roots_iterator, "xcb_setup_roots_iterator" }, + { (dyn::apiproc *) &generate_id, "xcb_generate_id" }, + }; + + if (dyn::load(handle, funcs)) { + return -1; + } + + funcs_loaded = true; + return 0; + } + +#undef _FN + } // namespace xcb + + void + freeImage(XImage *); + void + freeX(XFixesCursorImage *); + + using xcb_connect_t = util::dyn_safe_ptr; + using xcb_img_t = util::c_ptr; + + using ximg_t = util::safe_ptr; + using xcursor_t = util::safe_ptr; + + using crtc_info_t = util::dyn_safe_ptr<_XRRCrtcInfo, &x11::rr::FreeCrtcInfo>; + using output_info_t = util::dyn_safe_ptr<_XRROutputInfo, &x11::rr::FreeOutputInfo>; + using screen_res_t = util::dyn_safe_ptr<_XRRScreenResources, &x11::rr::FreeScreenResources>; + + class shm_id_t { + public: + shm_id_t(): + id { -1 } {} + shm_id_t(int id): + id { id } {} + shm_id_t(shm_id_t &&other) noexcept: + id(other.id) { + other.id = -1; + } + + ~shm_id_t() { + if (id != -1) { + shmctl(id, IPC_RMID, nullptr); + id = -1; + } + } + int id; + }; + + class shm_data_t { + public: + shm_data_t(): + data { (void *) -1 } {} + shm_data_t(void *data): + data { data } {} + + shm_data_t(shm_data_t &&other) noexcept: + data(other.data) { + other.data = (void *) -1; + } + + ~shm_data_t() { + if ((std::uintptr_t) data != -1) { + shmdt(data); + } + } + + void *data; + }; + + struct x11_img_t: public img_t { + ximg_t img; + }; + + struct shm_img_t: public img_t { + ~shm_img_t() override { + delete[] data; + data = nullptr; + } + }; + + static void + blend_cursor(Display *display, img_t &img, int offsetX, int offsetY) { + xcursor_t overlay { x11::fix::GetCursorImage(display) }; + + if (!overlay) { + BOOST_LOG(error) << "Couldn't get cursor from XFixesGetCursorImage"sv; + return; + } + + overlay->x -= overlay->xhot; + overlay->y -= overlay->yhot; + + overlay->x -= offsetX; + overlay->y -= offsetY; + + overlay->x = std::max((short) 0, overlay->x); + overlay->y = std::max((short) 0, overlay->y); + + auto pixels = (int *) img.data; + + auto screen_height = img.height; + auto screen_width = img.width; + + auto delta_height = std::min(overlay->height, std::max(0, screen_height - overlay->y)); + auto delta_width = std::min(overlay->width, std::max(0, screen_width - overlay->x)); + for (auto y = 0; y < delta_height; ++y) { + auto overlay_begin = &overlay->pixels[y * overlay->width]; + auto overlay_end = &overlay->pixels[y * overlay->width + delta_width]; + + auto pixels_begin = &pixels[(y + overlay->y) * (img.row_pitch / img.pixel_pitch) + overlay->x]; + + std::for_each(overlay_begin, overlay_end, [&](long pixel) { + int *pixel_p = (int *) &pixel; + + auto colors_in = (uint8_t *) pixels_begin; + + auto alpha = (*(uint *) pixel_p) >> 24u; + if (alpha == 255) { + *pixels_begin = *pixel_p; + } + else { + auto colors_out = (uint8_t *) pixel_p; + colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255; + colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255; + colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255; + } + ++pixels_begin; + }); + } + } + + struct x11_attr_t: public display_t { + std::chrono::nanoseconds delay; + + x11::xdisplay_t xdisplay; + Window xwindow; + XWindowAttributes xattr; + + mem_type_e mem_type; + + /* + * Last X (NOT the streamed monitor!) size. + * This way we can trigger reinitialization if the dimensions changed while streaming + */ + // int env_width, env_height; + + x11_attr_t(mem_type_e mem_type): + xdisplay { x11::OpenDisplay(nullptr) }, xwindow {}, xattr {}, mem_type { mem_type } { + x11::InitThreads(); + } + + int + init(const std::string &display_name, const ::video::config_t &config) { + if (!xdisplay) { + BOOST_LOG(error) << "Could not open X11 display"sv; + return -1; + } + + delay = std::chrono::nanoseconds { 1s } / config.framerate; + + xwindow = DefaultRootWindow(xdisplay.get()); + + refresh(); + + int streamedMonitor = -1; + if (!display_name.empty()) { + streamedMonitor = (int) util::from_view(display_name); + } + + if (streamedMonitor != -1) { + BOOST_LOG(info) << "Configuring selected monitor ("sv << streamedMonitor << ") to stream"sv; + screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) }; + int output = screenr->noutput; + + output_info_t result; + int monitor = 0; + for (int x = 0; x < output; ++x) { + output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) }; + if (out_info && out_info->connection == RR_Connected) { + if (monitor++ == streamedMonitor) { + result = std::move(out_info); + break; + } + } + } + + if (!result) { + BOOST_LOG(error) << "Could not stream display number ["sv << streamedMonitor << "], there are only ["sv << monitor << "] displays."sv; + return -1; + } + + if (result->crtc) { + crtc_info_t crt_info { x11::rr::GetCrtcInfo(xdisplay.get(), screenr.get(), result->crtc) }; + BOOST_LOG(info) + << "Streaming display: "sv << result->name << " with res "sv << crt_info->width << 'x' << crt_info->height << " offset by "sv << crt_info->x << 'x' << crt_info->y; + + width = crt_info->width; + height = crt_info->height; + offset_x = crt_info->x; + offset_y = crt_info->y; + } + else { + BOOST_LOG(warning) << "Couldn't get requested display info, defaulting to recording entire virtual desktop"sv; + width = xattr.width; + height = xattr.height; + } } else { - BOOST_LOG(warning) << "Couldn't get requested display info, defaulting to recording entire virtual desktop"sv; - width = xattr.width; + width = xattr.width; height = xattr.height; } - } - else { - width = xattr.width; - height = xattr.height; + + env_width = xattr.width; + env_height = xattr.height; + + return 0; } - env_width = xattr.width; - env_height = xattr.height; - - return 0; - } - - /** + /** * Called when the display attributes should change. */ - void refresh() { - x11::GetWindowAttributes(xdisplay.get(), xwindow, &xattr); //Update xattr's - } - - capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { - auto next_frame = std::chrono::steady_clock::now(); - - while(img) { - auto now = std::chrono::steady_clock::now(); - - if(next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); - } - while(next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); - } - next_frame = now + delay; - - auto status = snapshot(img.get(), 1000ms, *cursor); - switch(status) { - case platf::capture_e::reinit: - case platf::capture_e::error: - return status; - case platf::capture_e::timeout: - img = snapshot_cb(img, false); - break; - case platf::capture_e::ok: - img = snapshot_cb(img, true); - break; - default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']'; - return status; - } + void + refresh() { + x11::GetWindowAttributes(xdisplay.get(), xwindow, &xattr); //Update xattr's } - return capture_e::ok; - } + capture_e + capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { + auto next_frame = std::chrono::steady_clock::now(); - capture_e snapshot(img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { - refresh(); + while (img) { + auto now = std::chrono::steady_clock::now(); - //The whole X server changed, so we must reinit everything - if(xattr.width != env_width || xattr.height != env_height) { - BOOST_LOG(warning) << "X dimensions changed in non-SHM mode, request reinit"sv; - return capture_e::reinit; - } - XImage *img { x11::GetImage(xdisplay.get(), xwindow, offset_x, offset_y, width, height, AllPlanes, ZPixmap) }; + if (next_frame > now) { + std::this_thread::sleep_for((next_frame - now) / 3 * 2); + } + while (next_frame > now) { + std::this_thread::sleep_for(1ns); + now = std::chrono::steady_clock::now(); + } + next_frame = now + delay; - auto img_out = (x11_img_t *)img_out_base; - img_out->width = img->width; - img_out->height = img->height; - img_out->data = (uint8_t *)img->data; - img_out->row_pitch = img->bytes_per_line; - img_out->pixel_pitch = img->bits_per_pixel / 8; - img_out->img.reset(img); - - if(cursor) { - blend_cursor(xdisplay.get(), *img_out_base, offset_x, offset_y); - } - - return capture_e::ok; - } - - std::shared_ptr alloc_img() override { - return std::make_shared(); - } - - std::shared_ptr make_hwdevice(pix_fmt_e pix_fmt) override { - if(mem_type == mem_type_e::vaapi) { - return va::make_hwdevice(width, height, false); - } - -#ifdef SUNSHINE_BUILD_CUDA - if(mem_type == mem_type_e::cuda) { - return cuda::make_hwdevice(width, height, false); - } -#endif - - return std::make_shared(); - } - - int dummy_img(img_t *img) override { - snapshot(img, 0s, true); - return 0; - } -}; - -struct shm_attr_t : public x11_attr_t { - x11::xdisplay_t shm_xdisplay; // Prevent race condition with x11_attr_t::xdisplay - xcb_connect_t xcb; - xcb_screen_t *display; - std::uint32_t seg; - - shm_id_t shm_id; - - shm_data_t data; - - task_pool_util::TaskPool::task_id_t refresh_task_id; - - void delayed_refresh() { - refresh(); - - refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id; - } - - shm_attr_t(mem_type_e mem_type) : x11_attr_t(mem_type), shm_xdisplay { x11::OpenDisplay(nullptr) } { - refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id; - } - - ~shm_attr_t() override { - while(!task_pool.cancel(refresh_task_id)) - ; - } - - capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { - auto next_frame = std::chrono::steady_clock::now(); - - while(img) { - auto now = std::chrono::steady_clock::now(); - - if(next_frame > now) { - std::this_thread::sleep_for((next_frame - now) / 3 * 2); - } - while(next_frame > now) { - std::this_thread::sleep_for(1ns); - now = std::chrono::steady_clock::now(); - } - next_frame = now + delay; - - auto status = snapshot(img.get(), 1000ms, *cursor); - switch(status) { - case platf::capture_e::reinit: - case platf::capture_e::error: - return status; - case platf::capture_e::timeout: - img = snapshot_cb(img, false); - break; - case platf::capture_e::ok: - img = snapshot_cb(img, true); - break; - default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']'; - return status; - } - } - - return capture_e::ok; - } - - capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor) { - //The whole X server changed, so we must reinit everything - if(xattr.width != env_width || xattr.height != env_height) { - BOOST_LOG(warning) << "X dimensions changed in SHM mode, request reinit"sv; - return capture_e::reinit; - } - else { - auto img_cookie = xcb::shm_get_image_unchecked(xcb.get(), display->root, offset_x, offset_y, width, height, ~0, XCB_IMAGE_FORMAT_Z_PIXMAP, seg, 0); - - xcb_img_t img_reply { xcb::shm_get_image_reply(xcb.get(), img_cookie, nullptr) }; - if(!img_reply) { - BOOST_LOG(error) << "Could not get image reply"sv; - return capture_e::reinit; - } - - std::copy_n((std::uint8_t *)data.data, frame_size(), img->data); - - if(cursor) { - blend_cursor(shm_xdisplay.get(), *img, offset_x, offset_y); + auto status = snapshot(img.get(), 1000ms, *cursor); + switch (status) { + case platf::capture_e::reinit: + case platf::capture_e::error: + return status; + case platf::capture_e::timeout: + img = snapshot_cb(img, false); + break; + case platf::capture_e::ok: + img = snapshot_cb(img, true); + break; + default: + BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; + return status; + } } return capture_e::ok; } - } - std::shared_ptr alloc_img() override { - auto img = std::make_shared(); - img->width = width; - img->height = height; - img->pixel_pitch = 4; - img->row_pitch = img->pixel_pitch * width; - img->data = new std::uint8_t[height * img->row_pitch]; + capture_e + snapshot(img_t *img_out_base, std::chrono::milliseconds timeout, bool cursor) { + refresh(); - return img; - } + //The whole X server changed, so we must reinit everything + if (xattr.width != env_width || xattr.height != env_height) { + BOOST_LOG(warning) << "X dimensions changed in non-SHM mode, request reinit"sv; + return capture_e::reinit; + } + XImage *img { x11::GetImage(xdisplay.get(), xwindow, offset_x, offset_y, width, height, AllPlanes, ZPixmap) }; - int dummy_img(platf::img_t *img) override { - return 0; - } + auto img_out = (x11_img_t *) img_out_base; + img_out->width = img->width; + img_out->height = img->height; + img_out->data = (uint8_t *) img->data; + img_out->row_pitch = img->bytes_per_line; + img_out->pixel_pitch = img->bits_per_pixel / 8; + img_out->img.reset(img); - int init(const std::string &display_name, const ::video::config_t &config) { - if(x11_attr_t::init(display_name, config)) { - return 1; + if (cursor) { + blend_cursor(xdisplay.get(), *img_out_base, offset_x, offset_y); + } + + return capture_e::ok; } - shm_xdisplay.reset(x11::OpenDisplay(nullptr)); - xcb.reset(xcb::connect(nullptr, nullptr)); - if(xcb::connection_has_error(xcb.get())) { - return -1; + std::shared_ptr + alloc_img() override { + return std::make_shared(); } - if(!xcb::get_extension_data(xcb.get(), xcb::shm_id)->present) { - BOOST_LOG(error) << "Missing SHM extension"sv; + std::shared_ptr + make_hwdevice(pix_fmt_e pix_fmt) override { + if (mem_type == mem_type_e::vaapi) { + return va::make_hwdevice(width, height, false); + } - return -1; +#ifdef SUNSHINE_BUILD_CUDA + if (mem_type == mem_type_e::cuda) { + return cuda::make_hwdevice(width, height, false); + } +#endif + + return std::make_shared(); } - auto iter = xcb::setup_roots_iterator(xcb::get_setup(xcb.get())); - display = iter.data; - seg = xcb::generate_id(xcb.get()); + int + dummy_img(img_t *img) override { + snapshot(img, 0s, true); + return 0; + } + }; - shm_id.id = shmget(IPC_PRIVATE, frame_size(), IPC_CREAT | 0777); - if(shm_id.id == -1) { - BOOST_LOG(error) << "shmget failed"sv; - return -1; + struct shm_attr_t: public x11_attr_t { + x11::xdisplay_t shm_xdisplay; // Prevent race condition with x11_attr_t::xdisplay + xcb_connect_t xcb; + xcb_screen_t *display; + std::uint32_t seg; + + shm_id_t shm_id; + + shm_data_t data; + + task_pool_util::TaskPool::task_id_t refresh_task_id; + + void + delayed_refresh() { + refresh(); + + refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id; } - xcb::shm_attach(xcb.get(), seg, shm_id.id, false); - data.data = shmat(shm_id.id, nullptr, 0); - - if((uintptr_t)data.data == -1) { - BOOST_LOG(error) << "shmat failed"sv; - - return -1; + shm_attr_t(mem_type_e mem_type): + x11_attr_t(mem_type), shm_xdisplay { x11::OpenDisplay(nullptr) } { + refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id; } - return 0; - } - - std::uint32_t frame_size() { - return width * height * 4; - } -}; - -std::shared_ptr x11_display(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { - if(hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) { - BOOST_LOG(error) << "Could not initialize x11 display with the given hw device type"sv; - return nullptr; - } - - if(xcb::init_shm() || xcb::init() || x11::init() || x11::rr::init() || x11::fix::init()) { - BOOST_LOG(error) << "Couldn't init x11 libraries"sv; - - return nullptr; - } - - // Attempt to use shared memory X11 to avoid copying the frame - auto shm_disp = std::make_shared(hwdevice_type); - - auto status = shm_disp->init(display_name, config); - if(status > 0) { - // x11_attr_t::init() failed, don't bother trying again. - return nullptr; - } - - if(status == 0) { - return shm_disp; - } - - // Fallback - auto x11_disp = std::make_shared(hwdevice_type); - if(x11_disp->init(display_name, config)) { - return nullptr; - } - - return x11_disp; -} - -std::vector x11_display_names() { - if(load_x11() || load_xcb()) { - BOOST_LOG(error) << "Couldn't init x11 libraries"sv; - - return {}; - } - - BOOST_LOG(info) << "Detecting connected monitors"sv; - - x11::xdisplay_t xdisplay { x11::OpenDisplay(nullptr) }; - if(!xdisplay) { - return {}; - } - - auto xwindow = DefaultRootWindow(xdisplay.get()); - screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) }; - int output = screenr->noutput; - - int monitor = 0; - for(int x = 0; x < output; ++x) { - output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) }; - if(out_info && out_info->connection == RR_Connected) { - ++monitor; - } - } - - std::vector names; - names.reserve(monitor); - - for(auto x = 0; x < monitor; ++x) { - names.emplace_back(std::to_string(x)); - } - - return names; -} - -void freeImage(XImage *p) { - XDestroyImage(p); -} -void freeX(XFixesCursorImage *p) { - x11::Free(p); -} - -int load_xcb() { - // This will be called once only - static int xcb_status = xcb::init_shm() || xcb::init(); - - return xcb_status; -} - -int load_x11() { - // This will be called once only - static int x11_status = - window_system == window_system_e::NONE || - x11::init() || x11::rr::init() || x11::fix::init(); - - return x11_status; -} - -namespace x11 { -std::optional cursor_t::make() { - if(load_x11()) { - return std::nullopt; - } - - cursor_t cursor; - - cursor.ctx.reset((cursor_ctx_t::pointer)x11::OpenDisplay(nullptr)); - - return cursor; -} - -void cursor_t::capture(egl::cursor_t &img) { - auto display = (xdisplay_t::pointer)ctx.get(); - - xcursor_t xcursor = fix::GetCursorImage(display); - - if(img.serial != xcursor->cursor_serial) { - auto buf_size = xcursor->width * xcursor->height * sizeof(int); - - if(img.buffer.size() < buf_size) { - img.buffer.resize(buf_size); + ~shm_attr_t() override { + while (!task_pool.cancel(refresh_task_id)) + ; } - std::transform(xcursor->pixels, xcursor->pixels + buf_size / 4, (int *)img.buffer.data(), [](long pixel) -> int { - return pixel; - }); + capture_e + capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { + auto next_frame = std::chrono::steady_clock::now(); + + while (img) { + auto now = std::chrono::steady_clock::now(); + + if (next_frame > now) { + std::this_thread::sleep_for((next_frame - now) / 3 * 2); + } + while (next_frame > now) { + std::this_thread::sleep_for(1ns); + now = std::chrono::steady_clock::now(); + } + next_frame = now + delay; + + auto status = snapshot(img.get(), 1000ms, *cursor); + switch (status) { + case platf::capture_e::reinit: + case platf::capture_e::error: + return status; + case platf::capture_e::timeout: + img = snapshot_cb(img, false); + break; + case platf::capture_e::ok: + img = snapshot_cb(img, true); + break; + default: + BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; + return status; + } + } + + return capture_e::ok; + } + + capture_e + snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor) { + //The whole X server changed, so we must reinit everything + if (xattr.width != env_width || xattr.height != env_height) { + BOOST_LOG(warning) << "X dimensions changed in SHM mode, request reinit"sv; + return capture_e::reinit; + } + else { + auto img_cookie = xcb::shm_get_image_unchecked(xcb.get(), display->root, offset_x, offset_y, width, height, ~0, XCB_IMAGE_FORMAT_Z_PIXMAP, seg, 0); + + xcb_img_t img_reply { xcb::shm_get_image_reply(xcb.get(), img_cookie, nullptr) }; + if (!img_reply) { + BOOST_LOG(error) << "Could not get image reply"sv; + return capture_e::reinit; + } + + std::copy_n((std::uint8_t *) data.data, frame_size(), img->data); + + if (cursor) { + blend_cursor(shm_xdisplay.get(), *img, offset_x, offset_y); + } + + return capture_e::ok; + } + } + + std::shared_ptr + alloc_img() override { + auto img = std::make_shared(); + img->width = width; + img->height = height; + img->pixel_pitch = 4; + img->row_pitch = img->pixel_pitch * width; + img->data = new std::uint8_t[height * img->row_pitch]; + + return img; + } + + int + dummy_img(platf::img_t *img) override { + return 0; + } + + int + init(const std::string &display_name, const ::video::config_t &config) { + if (x11_attr_t::init(display_name, config)) { + return 1; + } + + shm_xdisplay.reset(x11::OpenDisplay(nullptr)); + xcb.reset(xcb::connect(nullptr, nullptr)); + if (xcb::connection_has_error(xcb.get())) { + return -1; + } + + if (!xcb::get_extension_data(xcb.get(), xcb::shm_id)->present) { + BOOST_LOG(error) << "Missing SHM extension"sv; + + return -1; + } + + auto iter = xcb::setup_roots_iterator(xcb::get_setup(xcb.get())); + display = iter.data; + seg = xcb::generate_id(xcb.get()); + + shm_id.id = shmget(IPC_PRIVATE, frame_size(), IPC_CREAT | 0777); + if (shm_id.id == -1) { + BOOST_LOG(error) << "shmget failed"sv; + return -1; + } + + xcb::shm_attach(xcb.get(), seg, shm_id.id, false); + data.data = shmat(shm_id.id, nullptr, 0); + + if ((uintptr_t) data.data == -1) { + BOOST_LOG(error) << "shmat failed"sv; + + return -1; + } + + return 0; + } + + std::uint32_t + frame_size() { + return width * height * 4; + } + }; + + std::shared_ptr + x11_display(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) { + if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) { + BOOST_LOG(error) << "Could not initialize x11 display with the given hw device type"sv; + return nullptr; + } + + if (xcb::init_shm() || xcb::init() || x11::init() || x11::rr::init() || x11::fix::init()) { + BOOST_LOG(error) << "Couldn't init x11 libraries"sv; + + return nullptr; + } + + // Attempt to use shared memory X11 to avoid copying the frame + auto shm_disp = std::make_shared(hwdevice_type); + + auto status = shm_disp->init(display_name, config); + if (status > 0) { + // x11_attr_t::init() failed, don't bother trying again. + return nullptr; + } + + if (status == 0) { + return shm_disp; + } + + // Fallback + auto x11_disp = std::make_shared(hwdevice_type); + if (x11_disp->init(display_name, config)) { + return nullptr; + } + + return x11_disp; } - img.data = img.buffer.data(); - img.width = xcursor->width; - img.height = xcursor->height; - img.x = xcursor->x - xcursor->xhot; - img.y = xcursor->y - xcursor->yhot; - img.pixel_pitch = 4; - img.row_pitch = img.pixel_pitch * img.width; - img.serial = xcursor->cursor_serial; -} + std::vector + x11_display_names() { + if (load_x11() || load_xcb()) { + BOOST_LOG(error) << "Couldn't init x11 libraries"sv; -void cursor_t::blend(img_t &img, int offsetX, int offsetY) { - blend_cursor((xdisplay_t::pointer)ctx.get(), img, offsetX, offsetY); -} + return {}; + } -xdisplay_t make_display() { - return OpenDisplay(nullptr); -} + BOOST_LOG(info) << "Detecting connected monitors"sv; -void freeDisplay(_XDisplay *xdisplay) { - CloseDisplay(xdisplay); -} + x11::xdisplay_t xdisplay { x11::OpenDisplay(nullptr) }; + if (!xdisplay) { + return {}; + } -void freeCursorCtx(cursor_ctx_t::pointer ctx) { - CloseDisplay((xdisplay_t::pointer)ctx); -} -} // namespace x11 -} // namespace platf + auto xwindow = DefaultRootWindow(xdisplay.get()); + screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) }; + int output = screenr->noutput; + + int monitor = 0; + for (int x = 0; x < output; ++x) { + output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) }; + if (out_info && out_info->connection == RR_Connected) { + ++monitor; + } + } + + std::vector names; + names.reserve(monitor); + + for (auto x = 0; x < monitor; ++x) { + names.emplace_back(std::to_string(x)); + } + + return names; + } + + void + freeImage(XImage *p) { + XDestroyImage(p); + } + void + freeX(XFixesCursorImage *p) { + x11::Free(p); + } + + int + load_xcb() { + // This will be called once only + static int xcb_status = xcb::init_shm() || xcb::init(); + + return xcb_status; + } + + int + load_x11() { + // This will be called once only + static int x11_status = + window_system == window_system_e::NONE || + x11::init() || x11::rr::init() || x11::fix::init(); + + return x11_status; + } + + namespace x11 { + std::optional + cursor_t::make() { + if (load_x11()) { + return std::nullopt; + } + + cursor_t cursor; + + cursor.ctx.reset((cursor_ctx_t::pointer) x11::OpenDisplay(nullptr)); + + return cursor; + } + + void + cursor_t::capture(egl::cursor_t &img) { + auto display = (xdisplay_t::pointer) ctx.get(); + + xcursor_t xcursor = fix::GetCursorImage(display); + + if (img.serial != xcursor->cursor_serial) { + auto buf_size = xcursor->width * xcursor->height * sizeof(int); + + if (img.buffer.size() < buf_size) { + img.buffer.resize(buf_size); + } + + std::transform(xcursor->pixels, xcursor->pixels + buf_size / 4, (int *) img.buffer.data(), [](long pixel) -> int { + return pixel; + }); + } + + img.data = img.buffer.data(); + img.width = xcursor->width; + img.height = xcursor->height; + img.x = xcursor->x - xcursor->xhot; + img.y = xcursor->y - xcursor->yhot; + img.pixel_pitch = 4; + img.row_pitch = img.pixel_pitch * img.width; + img.serial = xcursor->cursor_serial; + } + + void + cursor_t::blend(img_t &img, int offsetX, int offsetY) { + blend_cursor((xdisplay_t::pointer) ctx.get(), img, offsetX, offsetY); + } + + xdisplay_t + make_display() { + return OpenDisplay(nullptr); + } + + void + freeDisplay(_XDisplay *xdisplay) { + CloseDisplay(xdisplay); + } + + void + freeCursorCtx(cursor_ctx_t::pointer ctx) { + CloseDisplay((xdisplay_t::pointer) ctx); + } + } // namespace x11 +} // namespace platf diff --git a/src/platform/linux/x11grab.h b/src/platform/linux/x11grab.h index 801daad5..89428a49 100644 --- a/src/platform/linux/x11grab.h +++ b/src/platform/linux/x11grab.h @@ -10,51 +10,61 @@ extern "C" struct _XDisplay; namespace egl { -class cursor_t; + class cursor_t; } namespace platf::x11 { #ifdef SUNSHINE_BUILD_X11 -struct cursor_ctx_raw_t; -void freeCursorCtx(cursor_ctx_raw_t *ctx); -void freeDisplay(_XDisplay *xdisplay); + struct cursor_ctx_raw_t; + void + freeCursorCtx(cursor_ctx_raw_t *ctx); + void + freeDisplay(_XDisplay *xdisplay); -using cursor_ctx_t = util::safe_ptr; -using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>; + using cursor_ctx_t = util::safe_ptr; + using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>; -class cursor_t { -public: - static std::optional make(); + class cursor_t { + public: + static std::optional + make(); - void capture(egl::cursor_t &img); + void + capture(egl::cursor_t &img); - /** + /** * Capture and blend the cursor into the image * * img <-- destination image * offsetX, offsetY <--- Top left corner of the virtual screen */ - void blend(img_t &img, int offsetX, int offsetY); + void + blend(img_t &img, int offsetX, int offsetY); - cursor_ctx_t ctx; -}; + cursor_ctx_t ctx; + }; -xdisplay_t make_display(); + xdisplay_t + make_display(); #else -// It's never something different from nullptr -util::safe_ptr<_XDisplay, std::default_delete<_XDisplay>>; + // It's never something different from nullptr + util::safe_ptr<_XDisplay, std::default_delete<_XDisplay>>; -class cursor_t { -public: - static std::optional make() { return std::nullopt; } + class cursor_t { + public: + static std::optional + make() { return std::nullopt; } - void capture(egl::cursor_t &) {} - void blend(img_t &, int, int) {} -}; + void + capture(egl::cursor_t &) {} + void + blend(img_t &, int, int) {} + }; -xdisplay_t make_display() { return nullptr; } + xdisplay_t + make_display() { return nullptr; } #endif -} // namespace platf::x11 +} // namespace platf::x11 #endif \ No newline at end of file diff --git a/src/platform/macos/av_audio.h b/src/platform/macos/av_audio.h index 2a0b5a43..48a83ac4 100644 --- a/src/platform/macos/av_audio.h +++ b/src/platform/macos/av_audio.h @@ -7,14 +7,14 @@ #define kBufferLength 2048 -@interface AVAudio : NSObject { +@interface AVAudio: NSObject { @public TPCircularBuffer audioSampleBuffer; } -@property(nonatomic, assign) AVCaptureSession *audioCaptureSession; -@property(nonatomic, assign) AVCaptureConnection *audioConnection; -@property(nonatomic, assign) NSCondition *samplesArrivedSignal; +@property (nonatomic, assign) AVCaptureSession *audioCaptureSession; +@property (nonatomic, assign) AVCaptureConnection *audioConnection; +@property (nonatomic, assign) NSCondition *samplesArrivedSignal; + (NSArray *)microphoneNames; + (AVCaptureDevice *)findMicrophone:(NSString *)name; @@ -23,4 +23,4 @@ @end -#endif //SUNSHINE_PLATFORM_AV_AUDIO_H +#endif //SUNSHINE_PLATFORM_AV_AUDIO_H diff --git a/src/platform/macos/av_audio.m b/src/platform/macos/av_audio.m index 43f726e4..ef1ce294 100644 --- a/src/platform/macos/av_audio.m +++ b/src/platform/macos/av_audio.m @@ -3,8 +3,7 @@ @implementation AVAudio + (NSArray *)microphones { - - if([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:((NSOperatingSystemVersion) { 10, 15, 0 })]) { + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:((NSOperatingSystemVersion) { 10, 15, 0 })]) { // This will generate a warning about AVCaptureDeviceDiscoverySession being // unavailable before macOS 10.15, but we have a guard to prevent it from // being called on those earlier systems. @@ -34,7 +33,7 @@ + (NSArray *)microphoneNames { NSMutableArray *result = [[NSMutableArray alloc] init]; - for(AVCaptureDevice *device in [AVAudio microphones]) { + for (AVCaptureDevice *device in [AVAudio microphones]) { [result addObject:[device localizedName]]; } @@ -42,8 +41,8 @@ } + (AVCaptureDevice *)findMicrophone:(NSString *)name { - for(AVCaptureDevice *device in [AVAudio microphones]) { - if([[device localizedName] isEqualToString:name]) { + for (AVCaptureDevice *device in [AVAudio microphones]) { + if ([[device localizedName] isEqualToString:name]) { return device; } } @@ -66,11 +65,11 @@ NSError *error; AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error]; - if(audioInput == nil) { + if (audioInput == nil) { return -1; } - if([self.audioCaptureSession canAddInput:audioInput]) { + if ([self.audioCaptureSession canAddInput:audioInput]) { [self.audioCaptureSession addInput:audioInput]; } else { @@ -81,22 +80,22 @@ AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init]; [audioOutput setAudioSettings:@{ - (NSString *)AVFormatIDKey: [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM], - (NSString *)AVSampleRateKey: [NSNumber numberWithUnsignedInt:sampleRate], - (NSString *)AVNumberOfChannelsKey: [NSNumber numberWithUnsignedInt:channels], - (NSString *)AVLinearPCMBitDepthKey: [NSNumber numberWithUnsignedInt:16], - (NSString *)AVLinearPCMIsFloatKey: @NO, - (NSString *)AVLinearPCMIsNonInterleaved: @NO + (NSString *) AVFormatIDKey: [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM], + (NSString *) AVSampleRateKey: [NSNumber numberWithUnsignedInt:sampleRate], + (NSString *) AVNumberOfChannelsKey: [NSNumber numberWithUnsignedInt:channels], + (NSString *) AVLinearPCMBitDepthKey: [NSNumber numberWithUnsignedInt:16], + (NSString *) AVLinearPCMIsFloatKey: @NO, + (NSString *) AVLinearPCMIsNonInterleaved: @NO }]; - dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, - QOS_CLASS_USER_INITIATED, - DISPATCH_QUEUE_PRIORITY_HIGH); + dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, + QOS_CLASS_USER_INITIATED, + DISPATCH_QUEUE_PRIORITY_HIGH); dispatch_queue_t recordingQueue = dispatch_queue_create("audioSamplingQueue", qos); [audioOutput setSampleBufferDelegate:self queue:recordingQueue]; - if([self.audioCaptureSession canAddOutput:audioOutput]) { + if ([self.audioCaptureSession canAddOutput:audioOutput]) { [self.audioCaptureSession addOutput:audioOutput]; } else { @@ -121,7 +120,7 @@ - (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { - if(connection == self.audioConnection) { + if (connection == self.audioConnection) { AudioBufferList audioBufferList; CMBlockBufferRef blockBuffer; diff --git a/src/platform/macos/av_img_t.h b/src/platform/macos/av_img_t.h index a7a24935..f946c6d5 100644 --- a/src/platform/macos/av_img_t.h +++ b/src/platform/macos/av_img_t.h @@ -7,12 +7,12 @@ #include namespace platf { -struct av_img_t : public img_t { - CVPixelBufferRef pixel_buffer = nullptr; - CMSampleBufferRef sample_buffer = nullptr; + struct av_img_t: public img_t { + CVPixelBufferRef pixel_buffer = nullptr; + CMSampleBufferRef sample_buffer = nullptr; - ~av_img_t(); -}; -} // namespace platf + ~av_img_t(); + }; +} // namespace platf #endif /* av_img_t_h */ diff --git a/src/platform/macos/av_video.h b/src/platform/macos/av_video.h index df441cae..d654ff67 100644 --- a/src/platform/macos/av_video.h +++ b/src/platform/macos/av_video.h @@ -3,33 +3,32 @@ #import - struct CaptureSession { AVCaptureVideoDataOutput *output; NSCondition *captureStopped; }; -@interface AVVideo : NSObject +@interface AVVideo: NSObject #define kMaxDisplays 32 -@property(nonatomic, assign) CGDirectDisplayID displayID; -@property(nonatomic, assign) CMTime minFrameDuration; -@property(nonatomic, assign) OSType pixelFormat; -@property(nonatomic, assign) int frameWidth; -@property(nonatomic, assign) int frameHeight; -@property(nonatomic, assign) float scaling; -@property(nonatomic, assign) int paddingLeft; -@property(nonatomic, assign) int paddingRight; -@property(nonatomic, assign) int paddingTop; -@property(nonatomic, assign) int paddingBottom; +@property (nonatomic, assign) CGDirectDisplayID displayID; +@property (nonatomic, assign) CMTime minFrameDuration; +@property (nonatomic, assign) OSType pixelFormat; +@property (nonatomic, assign) int frameWidth; +@property (nonatomic, assign) int frameHeight; +@property (nonatomic, assign) float scaling; +@property (nonatomic, assign) int paddingLeft; +@property (nonatomic, assign) int paddingRight; +@property (nonatomic, assign) int paddingTop; +@property (nonatomic, assign) int paddingBottom; typedef bool (^FrameCallbackBlock)(CMSampleBufferRef); -@property(nonatomic, assign) AVCaptureSession *session; -@property(nonatomic, assign) NSMapTable *videoOutputs; -@property(nonatomic, assign) NSMapTable *captureCallbacks; -@property(nonatomic, assign) NSMapTable *captureSignals; +@property (nonatomic, assign) AVCaptureSession *session; +@property (nonatomic, assign) NSMapTable *videoOutputs; +@property (nonatomic, assign) NSMapTable *captureCallbacks; +@property (nonatomic, assign) NSMapTable *captureSignals; + (NSArray *)displayNames; @@ -40,4 +39,4 @@ typedef bool (^FrameCallbackBlock)(CMSampleBufferRef); @end -#endif //SUNSHINE_PLATFORM_AV_VIDEO_H +#endif //SUNSHINE_PLATFORM_AV_VIDEO_H diff --git a/src/platform/macos/av_video.m b/src/platform/macos/av_video.m index f257d9c4..92f5ec08 100644 --- a/src/platform/macos/av_video.m +++ b/src/platform/macos/av_video.m @@ -10,13 +10,13 @@ + (NSArray *)displayNames { CGDirectDisplayID displays[kMaxDisplays]; uint32_t count; - if(CGGetActiveDisplayList(kMaxDisplays, displays, &count) != kCGErrorSuccess) { + if (CGGetActiveDisplayList(kMaxDisplays, displays, &count) != kCGErrorSuccess) { return [NSArray array]; } NSMutableArray *result = [NSMutableArray array]; - for(uint32_t i = 0; i < count; i++) { + for (uint32_t i = 0; i < count; i++) { [result addObject:@{ @"id": [NSNumber numberWithUnsignedInt:displays[i]], @"name": [NSString stringWithFormat:@"%d", displays[i]] @@ -31,27 +31,27 @@ CGDisplayModeRef mode = CGDisplayCopyDisplayMode(displayID); - self.displayID = displayID; - self.pixelFormat = kCVPixelFormatType_32BGRA; - self.frameWidth = CGDisplayModeGetPixelWidth(mode); - self.frameHeight = CGDisplayModeGetPixelHeight(mode); - self.scaling = CGDisplayPixelsWide(displayID) / CGDisplayModeGetPixelWidth(mode); - self.paddingLeft = 0; - self.paddingRight = 0; - self.paddingTop = 0; - self.paddingBottom = 0; + self.displayID = displayID; + self.pixelFormat = kCVPixelFormatType_32BGRA; + self.frameWidth = CGDisplayModeGetPixelWidth(mode); + self.frameHeight = CGDisplayModeGetPixelHeight(mode); + self.scaling = CGDisplayPixelsWide(displayID) / CGDisplayModeGetPixelWidth(mode); + self.paddingLeft = 0; + self.paddingRight = 0; + self.paddingTop = 0; + self.paddingBottom = 0; self.minFrameDuration = CMTimeMake(1, frameRate); - self.session = [[AVCaptureSession alloc] init]; - self.videoOutputs = [[NSMapTable alloc] init]; + self.session = [[AVCaptureSession alloc] init]; + self.videoOutputs = [[NSMapTable alloc] init]; self.captureCallbacks = [[NSMapTable alloc] init]; - self.captureSignals = [[NSMapTable alloc] init]; + self.captureSignals = [[NSMapTable alloc] init]; CFRelease(mode); AVCaptureScreenInput *screenInput = [[AVCaptureScreenInput alloc] initWithDisplayID:self.displayID]; [screenInput setMinFrameDuration:self.minFrameDuration]; - if([self.session canAddInput:screenInput]) { + if ([self.session canAddInput:screenInput]) { [self.session addInput:screenInput]; } else { @@ -75,40 +75,40 @@ - (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight { CGImageRef screenshot = CGDisplayCreateImage(self.displayID); - self.frameWidth = frameWidth; + self.frameWidth = frameWidth; self.frameHeight = frameHeight; - double screenRatio = (double)CGImageGetWidth(screenshot) / (double)CGImageGetHeight(screenshot); - double streamRatio = (double)frameWidth / (double)frameHeight; + double screenRatio = (double) CGImageGetWidth(screenshot) / (double) CGImageGetHeight(screenshot); + double streamRatio = (double) frameWidth / (double) frameHeight; - if(screenRatio < streamRatio) { - int padding = frameWidth - (frameHeight * screenRatio); - self.paddingLeft = padding / 2; - self.paddingRight = padding - self.paddingLeft; - self.paddingTop = 0; + if (screenRatio < streamRatio) { + int padding = frameWidth - (frameHeight * screenRatio); + self.paddingLeft = padding / 2; + self.paddingRight = padding - self.paddingLeft; + self.paddingTop = 0; self.paddingBottom = 0; } else { - int padding = frameHeight - (frameWidth / screenRatio); - self.paddingLeft = 0; - self.paddingRight = 0; - self.paddingTop = padding / 2; + int padding = frameHeight - (frameWidth / screenRatio); + self.paddingLeft = 0; + self.paddingRight = 0; + self.paddingTop = padding / 2; self.paddingBottom = padding - self.paddingTop; } // XXX: if the streamed image is larger than the native resolution, we add a black box around // the frame. Instead the frame should be resized entirely. int delta_width = frameWidth - (CGImageGetWidth(screenshot) + self.paddingLeft + self.paddingRight); - if(delta_width > 0) { - int adjust_left = delta_width / 2; + if (delta_width > 0) { + int adjust_left = delta_width / 2; int adjust_right = delta_width - adjust_left; self.paddingLeft += adjust_left; self.paddingRight += adjust_right; } int delta_height = frameHeight - (CGImageGetHeight(screenshot) + self.paddingTop + self.paddingBottom); - if(delta_height > 0) { - int adjust_top = delta_height / 2; + if (delta_height > 0) { + int adjust_top = delta_height / 2; int adjust_bottom = delta_height - adjust_top; self.paddingTop += adjust_top; self.paddingBottom += adjust_bottom; @@ -122,24 +122,24 @@ AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init]; [videoOutput setVideoSettings:@{ - (NSString *)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithUnsignedInt:self.pixelFormat], - (NSString *)kCVPixelBufferWidthKey: [NSNumber numberWithInt:self.frameWidth], - (NSString *)kCVPixelBufferExtendedPixelsRightKey: [NSNumber numberWithInt:self.paddingRight], - (NSString *)kCVPixelBufferExtendedPixelsLeftKey: [NSNumber numberWithInt:self.paddingLeft], - (NSString *)kCVPixelBufferExtendedPixelsTopKey: [NSNumber numberWithInt:self.paddingTop], - (NSString *)kCVPixelBufferExtendedPixelsBottomKey: [NSNumber numberWithInt:self.paddingBottom], - (NSString *)kCVPixelBufferHeightKey: [NSNumber numberWithInt:self.frameHeight] + (NSString *) kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithUnsignedInt:self.pixelFormat], + (NSString *) kCVPixelBufferWidthKey: [NSNumber numberWithInt:self.frameWidth], + (NSString *) kCVPixelBufferExtendedPixelsRightKey: [NSNumber numberWithInt:self.paddingRight], + (NSString *) kCVPixelBufferExtendedPixelsLeftKey: [NSNumber numberWithInt:self.paddingLeft], + (NSString *) kCVPixelBufferExtendedPixelsTopKey: [NSNumber numberWithInt:self.paddingTop], + (NSString *) kCVPixelBufferExtendedPixelsBottomKey: [NSNumber numberWithInt:self.paddingBottom], + (NSString *) kCVPixelBufferHeightKey: [NSNumber numberWithInt:self.frameHeight] }]; - dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, - QOS_CLASS_USER_INITIATED, - DISPATCH_QUEUE_PRIORITY_HIGH); + dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, + QOS_CLASS_USER_INITIATED, + DISPATCH_QUEUE_PRIORITY_HIGH); dispatch_queue_t recordingQueue = dispatch_queue_create("videoCaptureQueue", qos); [videoOutput setSampleBufferDelegate:self queue:recordingQueue]; [self.session stopRunning]; - if([self.session canAddOutput:videoOutput]) { + if ([self.session canAddOutput:videoOutput]) { [self.session addOutput:videoOutput]; } else { @@ -148,7 +148,7 @@ } AVCaptureConnection *videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo]; - dispatch_semaphore_t signal = dispatch_semaphore_create(0); + dispatch_semaphore_t signal = dispatch_semaphore_create(0); [self.videoOutputs setObject:videoOutput forKey:videoConnection]; [self.captureCallbacks setObject:frameCallback forKey:videoConnection]; @@ -163,11 +163,10 @@ - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { - FrameCallbackBlock callback = [self.captureCallbacks objectForKey:connection]; - if(callback != nil) { - if(!callback(sampleBuffer)) { + if (callback != nil) { + if (!callback(sampleBuffer)) { @synchronized(self) { [self.session stopRunning]; [self.captureCallbacks removeObjectForKey:connection]; diff --git a/src/platform/macos/display.mm b/src/platform/macos/display.mm index c818235e..f053ea03 100644 --- a/src/platform/macos/display.mm +++ b/src/platform/macos/display.mm @@ -14,189 +14,197 @@ namespace fs = std::filesystem; namespace platf { -using namespace std::literals; + using namespace std::literals; -av_img_t::~av_img_t() { - if(pixel_buffer != NULL) { - CVPixelBufferUnlockBaseAddress(pixel_buffer, 0); - } - - if(sample_buffer != nullptr) { - CFRelease(sample_buffer); - } - - data = nullptr; -} - -struct av_display_t : public display_t { - AVVideo *av_capture; - CGDirectDisplayID display_id; - - ~av_display_t() { - [av_capture release]; - } - - capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { - __block auto img_next = std::move(img); - - auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) { - auto av_img_next = std::static_pointer_cast(img_next); - - CFRetain(sampleBuffer); - - CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); - CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); - - if(av_img_next->pixel_buffer != nullptr) - CVPixelBufferUnlockBaseAddress(av_img_next->pixel_buffer, 0); - - if(av_img_next->sample_buffer != nullptr) - CFRelease(av_img_next->sample_buffer); - - av_img_next->sample_buffer = sampleBuffer; - av_img_next->pixel_buffer = pixelBuffer; - img_next->data = (uint8_t *)CVPixelBufferGetBaseAddress(pixelBuffer); - - size_t extraPixels[4]; - CVPixelBufferGetExtendedPixels(pixelBuffer, &extraPixels[0], &extraPixels[1], &extraPixels[2], &extraPixels[3]); - - img_next->width = CVPixelBufferGetWidth(pixelBuffer) + extraPixels[0] + extraPixels[1]; - img_next->height = CVPixelBufferGetHeight(pixelBuffer) + extraPixels[2] + extraPixels[3]; - img_next->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer); - img_next->pixel_pitch = img_next->row_pitch / img_next->width; - - img_next = snapshot_cb(img_next, true); - - return img_next != nullptr; - }]; - - // FIXME: We should time out if an image isn't returned for a while - dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER); - - return capture_e::ok; - } - - std::shared_ptr alloc_img() override { - return std::make_shared(); - } - - std::shared_ptr make_hwdevice(pix_fmt_e pix_fmt) override { - if(pix_fmt == pix_fmt_e::yuv420p) { - av_capture.pixelFormat = kCVPixelFormatType_32BGRA; - - return std::make_shared(); + av_img_t::~av_img_t() { + if (pixel_buffer != NULL) { + CVPixelBufferUnlockBaseAddress(pixel_buffer, 0); } - else if(pix_fmt == pix_fmt_e::nv12) { - auto device = std::make_shared(); - device->init(static_cast(av_capture), setResolution, setPixelFormat); + if (sample_buffer != nullptr) { + CFRelease(sample_buffer); + } - return device; - } - else { - BOOST_LOG(error) << "Unsupported Pixel Format."sv; - return nullptr; - } + data = nullptr; } - int dummy_img(img_t *img) override { - auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) { - auto av_img = (av_img_t *)img; + struct av_display_t: public display_t { + AVVideo *av_capture; + CGDirectDisplayID display_id; - CFRetain(sampleBuffer); + ~av_display_t() { + [av_capture release]; + } - CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); - CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + capture_e + capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override { + __block auto img_next = std::move(img); - // XXX: next_img->img should be moved to a smart pointer with - // the CFRelease as custom deallocator - if(av_img->pixel_buffer != nullptr) - CVPixelBufferUnlockBaseAddress(((av_img_t *)img)->pixel_buffer, 0); + auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) { + auto av_img_next = std::static_pointer_cast(img_next); - if(av_img->sample_buffer != nullptr) - CFRelease(av_img->sample_buffer); + CFRetain(sampleBuffer); - av_img->sample_buffer = sampleBuffer; - av_img->pixel_buffer = pixelBuffer; - img->data = (uint8_t *)CVPixelBufferGetBaseAddress(pixelBuffer); + CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); - size_t extraPixels[4]; - CVPixelBufferGetExtendedPixels(pixelBuffer, &extraPixels[0], &extraPixels[1], &extraPixels[2], &extraPixels[3]); + if (av_img_next->pixel_buffer != nullptr) + CVPixelBufferUnlockBaseAddress(av_img_next->pixel_buffer, 0); - img->width = CVPixelBufferGetWidth(pixelBuffer) + extraPixels[0] + extraPixels[1]; - img->height = CVPixelBufferGetHeight(pixelBuffer) + extraPixels[2] + extraPixels[3]; - img->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer); - img->pixel_pitch = img->row_pitch / img->width; + if (av_img_next->sample_buffer != nullptr) + CFRelease(av_img_next->sample_buffer); - return false; - }]; + av_img_next->sample_buffer = sampleBuffer; + av_img_next->pixel_buffer = pixelBuffer; + img_next->data = (uint8_t *) CVPixelBufferGetBaseAddress(pixelBuffer); - dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER); + size_t extraPixels[4]; + CVPixelBufferGetExtendedPixels(pixelBuffer, &extraPixels[0], &extraPixels[1], &extraPixels[2], &extraPixels[3]); - return 0; - } + img_next->width = CVPixelBufferGetWidth(pixelBuffer) + extraPixels[0] + extraPixels[1]; + img_next->height = CVPixelBufferGetHeight(pixelBuffer) + extraPixels[2] + extraPixels[3]; + img_next->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer); + img_next->pixel_pitch = img_next->row_pitch / img_next->width; - /** + img_next = snapshot_cb(img_next, true); + + return img_next != nullptr; + }]; + + // FIXME: We should time out if an image isn't returned for a while + dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER); + + return capture_e::ok; + } + + std::shared_ptr + alloc_img() override { + return std::make_shared(); + } + + std::shared_ptr + make_hwdevice(pix_fmt_e pix_fmt) override { + if (pix_fmt == pix_fmt_e::yuv420p) { + av_capture.pixelFormat = kCVPixelFormatType_32BGRA; + + return std::make_shared(); + } + else if (pix_fmt == pix_fmt_e::nv12) { + auto device = std::make_shared(); + + device->init(static_cast(av_capture), setResolution, setPixelFormat); + + return device; + } + else { + BOOST_LOG(error) << "Unsupported Pixel Format."sv; + return nullptr; + } + } + + int + dummy_img(img_t *img) override { + auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) { + auto av_img = (av_img_t *) img; + + CFRetain(sampleBuffer); + + CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + + // XXX: next_img->img should be moved to a smart pointer with + // the CFRelease as custom deallocator + if (av_img->pixel_buffer != nullptr) + CVPixelBufferUnlockBaseAddress(((av_img_t *) img)->pixel_buffer, 0); + + if (av_img->sample_buffer != nullptr) + CFRelease(av_img->sample_buffer); + + av_img->sample_buffer = sampleBuffer; + av_img->pixel_buffer = pixelBuffer; + img->data = (uint8_t *) CVPixelBufferGetBaseAddress(pixelBuffer); + + size_t extraPixels[4]; + CVPixelBufferGetExtendedPixels(pixelBuffer, &extraPixels[0], &extraPixels[1], &extraPixels[2], &extraPixels[3]); + + img->width = CVPixelBufferGetWidth(pixelBuffer) + extraPixels[0] + extraPixels[1]; + img->height = CVPixelBufferGetHeight(pixelBuffer) + extraPixels[2] + extraPixels[3]; + img->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer); + img->pixel_pitch = img->row_pitch / img->width; + + return false; + }]; + + dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER); + + return 0; + } + + /** * A bridge from the pure C++ code of the hwdevice_t class to the pure Objective C code. * * display --> an opaque pointer to an object of this class * width --> the intended capture width * height --> the intended capture height */ - static void setResolution(void *display, int width, int height) { - [static_cast(display) setFrameWidth:width frameHeight:height]; - } + static void + setResolution(void *display, int width, int height) { + [static_cast(display) setFrameWidth:width frameHeight:height]; + } - static void setPixelFormat(void *display, OSType pixelFormat) { - static_cast(display).pixelFormat = pixelFormat; - } -}; + static void + setPixelFormat(void *display, OSType pixelFormat) { + static_cast(display).pixelFormat = pixelFormat; + } + }; -std::shared_ptr display(platf::mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { - if(hwdevice_type != platf::mem_type_e::system) { - BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; - return nullptr; - } + std::shared_ptr + display(platf::mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { + if (hwdevice_type != platf::mem_type_e::system) { + BOOST_LOG(error) << "Could not initialize display with the given hw device type."sv; + return nullptr; + } - auto display = std::make_shared(); + auto display = std::make_shared(); - display->display_id = CGMainDisplayID(); - if(!display_name.empty()) { - auto display_array = [AVVideo displayNames]; + display->display_id = CGMainDisplayID(); + if (!display_name.empty()) { + auto display_array = [AVVideo displayNames]; - for(NSDictionary *item in display_array) { - NSString *name = item[@"name"]; - if(name.UTF8String == display_name) { - NSNumber *display_id = item[@"id"]; - display->display_id = [display_id unsignedIntValue]; + for (NSDictionary *item in display_array) { + NSString *name = item[@"name"]; + if (name.UTF8String == display_name) { + NSNumber *display_id = item[@"id"]; + display->display_id = [display_id unsignedIntValue]; + } } } + + display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate:config.framerate]; + + if (!display->av_capture) { + BOOST_LOG(error) << "Video setup failed."sv; + return nullptr; + } + + display->width = display->av_capture.frameWidth; + display->height = display->av_capture.frameHeight; + + return display; } - display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate:config.framerate]; + std::vector + display_names(mem_type_e hwdevice_type) { + __block std::vector display_names; - if(!display->av_capture) { - BOOST_LOG(error) << "Video setup failed."sv; - return nullptr; + auto display_array = [AVVideo displayNames]; + + display_names.reserve([display_array count]); + [display_array enumerateObjectsUsingBlock:^(NSDictionary *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + NSString *name = obj[@"name"]; + display_names.push_back(name.UTF8String); + }]; + + return display_names; } - - display->width = display->av_capture.frameWidth; - display->height = display->av_capture.frameHeight; - - return display; -} - -std::vector display_names(mem_type_e hwdevice_type) { - __block std::vector display_names; - - auto display_array = [AVVideo displayNames]; - - display_names.reserve([display_array count]); - [display_array enumerateObjectsUsingBlock:^(NSDictionary *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - NSString *name = obj[@"name"]; - display_names.push_back(name.UTF8String); - }]; - - return display_names; -} -} // namespace platf +} // namespace platf diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp index ff59f4fb..19948a95 100644 --- a/src/platform/macos/input.cpp +++ b/src/platform/macos/input.cpp @@ -11,36 +11,37 @@ #define MULTICLICK_DELAY_NS 500000000 namespace platf { -using namespace std::literals; + using namespace std::literals; -struct macos_input_t { -public: - CGDirectDisplayID display; - CGFloat displayScaling; - CGEventSourceRef source; + struct macos_input_t { + public: + CGDirectDisplayID display; + CGFloat displayScaling; + CGEventSourceRef source; - // keyboard related stuff - CGEventRef kb_event; - CGEventFlags kb_flags; + // keyboard related stuff + CGEventRef kb_event; + CGEventFlags kb_flags; - // mouse related stuff - CGEventRef mouse_event; // mouse event source - bool mouse_down[3]; // mouse button status - uint64_t last_mouse_event[3][2]; // timestamp of last mouse events -}; + // mouse related stuff + CGEventRef mouse_event; // mouse event source + bool mouse_down[3]; // mouse button status + uint64_t last_mouse_event[3][2]; // timestamp of last mouse events + }; -// A struct to hold a Windows keycode to Mac virtual keycode mapping. -struct KeyCodeMap { - int win_keycode; - int mac_keycode; -}; + // A struct to hold a Windows keycode to Mac virtual keycode mapping. + struct KeyCodeMap { + int win_keycode; + int mac_keycode; + }; -// Customized less operator for using std::lower_bound() on a KeyCodeMap array. -bool operator<(const KeyCodeMap &a, const KeyCodeMap &b) { - return a.win_keycode < b.win_keycode; -} + // Customized less operator for using std::lower_bound() on a KeyCodeMap array. + bool + operator<(const KeyCodeMap &a, const KeyCodeMap &b) { + return a.win_keycode < b.win_keycode; + } -// clang-format off + // clang-format off const KeyCodeMap kKeyCodesMap[] = { { 0x08 /* VKEY_BACK */, kVK_Delete }, { 0x09 /* VKEY_TAB */, kVK_Tab }, @@ -210,264 +211,281 @@ const KeyCodeMap kKeyCodesMap[] = { { 0xFD /* VKEY_PA1 */, -1 }, { 0xFE /* VKEY_OEM_CLEAR */, kVK_ANSI_KeypadClear } }; -// clang-format on + // clang-format on -int keysym(int keycode) { - KeyCodeMap key_map; + int + keysym(int keycode) { + KeyCodeMap key_map; - key_map.win_keycode = keycode; - const KeyCodeMap *temp_map = std::lower_bound( - kKeyCodesMap, kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]), key_map); + key_map.win_keycode = keycode; + const KeyCodeMap *temp_map = std::lower_bound( + kKeyCodesMap, kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]), key_map); - if(temp_map >= kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]) || - temp_map->win_keycode != keycode || temp_map->mac_keycode == -1) { + if (temp_map >= kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]) || + temp_map->win_keycode != keycode || temp_map->mac_keycode == -1) { + return -1; + } + + return temp_map->mac_keycode; + } + + void + keyboard(input_t &input, uint16_t modcode, bool release) { + auto key = keysym(modcode); + + BOOST_LOG(debug) << "got keycode: 0x"sv << std::hex << modcode << ", translated to: 0x" << std::hex << key << ", release:" << release; + + if (key < 0) { + return; + } + + auto macos_input = ((macos_input_t *) input.get()); + auto event = macos_input->kb_event; + + if (key == kVK_Shift || key == kVK_RightShift || + key == kVK_Command || key == kVK_RightCommand || + key == kVK_Option || key == kVK_RightOption || + key == kVK_Control || key == kVK_RightControl) { + CGEventFlags mask; + + switch (key) { + case kVK_Shift: + case kVK_RightShift: + mask = kCGEventFlagMaskShift; + break; + case kVK_Command: + case kVK_RightCommand: + mask = kCGEventFlagMaskCommand; + break; + case kVK_Option: + case kVK_RightOption: + mask = kCGEventFlagMaskAlternate; + break; + case kVK_Control: + case kVK_RightControl: + mask = kCGEventFlagMaskControl; + break; + } + + macos_input->kb_flags = release ? macos_input->kb_flags & ~mask : macos_input->kb_flags | mask; + CGEventSetType(event, kCGEventFlagsChanged); + CGEventSetFlags(event, macos_input->kb_flags); + } + else { + CGEventSetIntegerValueField(event, kCGKeyboardEventKeycode, key); + CGEventSetType(event, release ? kCGEventKeyUp : kCGEventKeyDown); + } + + CGEventPost(kCGHIDEventTap, event); + } + + void + unicode(input_t &input, char *utf8, int size) { + BOOST_LOG(info) << "unicode: Unicode input not yet implemented for MacOS."sv; + } + + int + alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) { + BOOST_LOG(info) << "alloc_gamepad: Gamepad not yet implemented for MacOS."sv; return -1; } - return temp_map->mac_keycode; -} - -void keyboard(input_t &input, uint16_t modcode, bool release) { - auto key = keysym(modcode); - - BOOST_LOG(debug) << "got keycode: 0x"sv << std::hex << modcode << ", translated to: 0x" << std::hex << key << ", release:" << release; - - if(key < 0) { - return; + void + free_gamepad(input_t &input, int nr) { + BOOST_LOG(info) << "free_gamepad: Gamepad not yet implemented for MacOS."sv; } - auto macos_input = ((macos_input_t *)input.get()); - auto event = macos_input->kb_event; + void + gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) { + BOOST_LOG(info) << "gamepad: Gamepad not yet implemented for MacOS."sv; + } - if(key == kVK_Shift || key == kVK_RightShift || - key == kVK_Command || key == kVK_RightCommand || - key == kVK_Option || key == kVK_RightOption || - key == kVK_Control || key == kVK_RightControl) { + // returns current mouse location: + inline CGPoint + get_mouse_loc(input_t &input) { + return CGEventGetLocation(((macos_input_t *) input.get())->mouse_event); + } - CGEventFlags mask; + void + post_mouse(input_t &input, CGMouseButton button, CGEventType type, CGPoint location, int click_count) { + BOOST_LOG(debug) << "mouse_event: "sv << button << ", type: "sv << type << ", location:"sv << location.x << ":"sv << location.y << " click_count: "sv << click_count; - switch(key) { - case kVK_Shift: - case kVK_RightShift: - mask = kCGEventFlagMaskShift; - break; - case kVK_Command: - case kVK_RightCommand: - mask = kCGEventFlagMaskCommand; - break; - case kVK_Option: - case kVK_RightOption: - mask = kCGEventFlagMaskAlternate; - break; - case kVK_Control: - case kVK_RightControl: - mask = kCGEventFlagMaskControl; - break; + auto macos_input = (macos_input_t *) input.get(); + auto display = macos_input->display; + auto event = macos_input->mouse_event; + + if (location.x < 0) + location.x = 0; + if (location.x >= CGDisplayPixelsWide(display)) + location.x = CGDisplayPixelsWide(display) - 1; + + if (location.y < 0) + location.y = 0; + if (location.y >= CGDisplayPixelsHigh(display)) + location.y = CGDisplayPixelsHigh(display) - 1; + + CGEventSetType(event, type); + CGEventSetLocation(event, location); + CGEventSetIntegerValueField(event, kCGMouseEventButtonNumber, button); + CGEventSetIntegerValueField(event, kCGMouseEventClickState, click_count); + + CGEventPost(kCGHIDEventTap, event); + + // For why this is here, see: + // https://stackoverflow.com/questions/15194409/simulated-mouseevent-not-working-properly-osx + CGWarpMouseCursorPosition(location); + } + + inline CGEventType + event_type_mouse(input_t &input) { + auto macos_input = ((macos_input_t *) input.get()); + + if (macos_input->mouse_down[0]) { + return kCGEventLeftMouseDragged; + } + else if (macos_input->mouse_down[1]) { + return kCGEventOtherMouseDragged; + } + else if (macos_input->mouse_down[2]) { + return kCGEventRightMouseDragged; + } + else { + return kCGEventMouseMoved; + } + } + + void + move_mouse(input_t &input, int deltaX, int deltaY) { + auto current = get_mouse_loc(input); + + CGPoint location = CGPointMake(current.x + deltaX, current.y + deltaY); + + post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, 0); + } + + void + abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { + auto scaling = ((macos_input_t *) input.get())->displayScaling; + + CGPoint location = CGPointMake(x * scaling, y * scaling); + + post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, 0); + } + + uint64_t + time_diff(uint64_t start) { + uint64_t elapsed; + Nanoseconds elapsedNano; + + elapsed = mach_absolute_time() - start; + elapsedNano = AbsoluteToNanoseconds(*(AbsoluteTime *) &elapsed); + + return *(uint64_t *) &elapsedNano; + } + + void + button_mouse(input_t &input, int button, bool release) { + CGMouseButton mac_button; + CGEventType event; + + auto mouse = ((macos_input_t *) input.get()); + + switch (button) { + case 1: + mac_button = kCGMouseButtonLeft; + event = release ? kCGEventLeftMouseUp : kCGEventLeftMouseDown; + break; + case 2: + mac_button = kCGMouseButtonCenter; + event = release ? kCGEventOtherMouseUp : kCGEventOtherMouseDown; + break; + case 3: + mac_button = kCGMouseButtonRight; + event = release ? kCGEventRightMouseUp : kCGEventRightMouseDown; + break; + default: + BOOST_LOG(warning) << "Unsupported mouse button for MacOS: "sv << button; + return; } - macos_input->kb_flags = release ? macos_input->kb_flags & ~mask : macos_input->kb_flags | mask; - CGEventSetType(event, kCGEventFlagsChanged); - CGEventSetFlags(event, macos_input->kb_flags); - } - else { - CGEventSetIntegerValueField(event, kCGKeyboardEventKeycode, key); - CGEventSetType(event, release ? kCGEventKeyUp : kCGEventKeyDown); + mouse->mouse_down[mac_button] = !release; + + // if the last mouse down was less than MULTICLICK_DELAY_NS, we send a double click event + if (time_diff(mouse->last_mouse_event[mac_button][release]) < MULTICLICK_DELAY_NS) { + post_mouse(input, mac_button, event, get_mouse_loc(input), 2); + } + else { + post_mouse(input, mac_button, event, get_mouse_loc(input), 1); + } + + mouse->last_mouse_event[mac_button][release] = mach_absolute_time(); } - CGEventPost(kCGHIDEventTap, event); -} - -void unicode(input_t &input, char *utf8, int size) { - BOOST_LOG(info) << "unicode: Unicode input not yet implemented for MacOS."sv; -} - -int alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) { - BOOST_LOG(info) << "alloc_gamepad: Gamepad not yet implemented for MacOS."sv; - return -1; -} - -void free_gamepad(input_t &input, int nr) { - BOOST_LOG(info) << "free_gamepad: Gamepad not yet implemented for MacOS."sv; -} - -void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) { - BOOST_LOG(info) << "gamepad: Gamepad not yet implemented for MacOS."sv; -} - -// returns current mouse location: -inline CGPoint get_mouse_loc(input_t &input) { - return CGEventGetLocation(((macos_input_t *)input.get())->mouse_event); -} - -void post_mouse(input_t &input, CGMouseButton button, CGEventType type, CGPoint location, int click_count) { - BOOST_LOG(debug) << "mouse_event: "sv << button << ", type: "sv << type << ", location:"sv << location.x << ":"sv << location.y << " click_count: "sv << click_count; - - auto macos_input = (macos_input_t *)input.get(); - auto display = macos_input->display; - auto event = macos_input->mouse_event; - - if(location.x < 0) - location.x = 0; - if(location.x >= CGDisplayPixelsWide(display)) - location.x = CGDisplayPixelsWide(display) - 1; - - if(location.y < 0) - location.y = 0; - if(location.y >= CGDisplayPixelsHigh(display)) - location.y = CGDisplayPixelsHigh(display) - 1; - - CGEventSetType(event, type); - CGEventSetLocation(event, location); - CGEventSetIntegerValueField(event, kCGMouseEventButtonNumber, button); - CGEventSetIntegerValueField(event, kCGMouseEventClickState, click_count); - - CGEventPost(kCGHIDEventTap, event); - - // For why this is here, see: - // https://stackoverflow.com/questions/15194409/simulated-mouseevent-not-working-properly-osx - CGWarpMouseCursorPosition(location); -} - -inline CGEventType event_type_mouse(input_t &input) { - auto macos_input = ((macos_input_t *)input.get()); - - if(macos_input->mouse_down[0]) { - return kCGEventLeftMouseDragged; - } - else if(macos_input->mouse_down[1]) { - return kCGEventOtherMouseDragged; - } - else if(macos_input->mouse_down[2]) { - return kCGEventRightMouseDragged; - } - else { - return kCGEventMouseMoved; - } -} - -void move_mouse(input_t &input, int deltaX, int deltaY) { - auto current = get_mouse_loc(input); - - CGPoint location = CGPointMake(current.x + deltaX, current.y + deltaY); - - post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, 0); -} - -void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { - auto scaling = ((macos_input_t *)input.get())->displayScaling; - - CGPoint location = CGPointMake(x * scaling, y * scaling); - - post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, 0); -} - -uint64_t time_diff(uint64_t start) { - uint64_t elapsed; - Nanoseconds elapsedNano; - - elapsed = mach_absolute_time() - start; - elapsedNano = AbsoluteToNanoseconds(*(AbsoluteTime *)&elapsed); - - return *(uint64_t *)&elapsedNano; -} - -void button_mouse(input_t &input, int button, bool release) { - CGMouseButton mac_button; - CGEventType event; - - auto mouse = ((macos_input_t *)input.get()); - - switch(button) { - case 1: - mac_button = kCGMouseButtonLeft; - event = release ? kCGEventLeftMouseUp : kCGEventLeftMouseDown; - break; - case 2: - mac_button = kCGMouseButtonCenter; - event = release ? kCGEventOtherMouseUp : kCGEventOtherMouseDown; - break; - case 3: - mac_button = kCGMouseButtonRight; - event = release ? kCGEventRightMouseUp : kCGEventRightMouseDown; - break; - default: - BOOST_LOG(warning) << "Unsupported mouse button for MacOS: "sv << button; - return; + void + scroll(input_t &input, int high_res_distance) { + CGEventRef upEvent = CGEventCreateScrollWheelEvent( + NULL, + kCGScrollEventUnitLine, + 2, high_res_distance > 0 ? 1 : -1, high_res_distance); + CGEventPost(kCGHIDEventTap, upEvent); + CFRelease(upEvent); } - mouse->mouse_down[mac_button] = !release; - - // if the last mouse down was less than MULTICLICK_DELAY_NS, we send a double click event - if(time_diff(mouse->last_mouse_event[mac_button][release]) < MULTICLICK_DELAY_NS) { - post_mouse(input, mac_button, event, get_mouse_loc(input), 2); - } - else { - post_mouse(input, mac_button, event, get_mouse_loc(input), 1); + void + hscroll(input_t &input, int high_res_distance) { + // Unimplemented } - mouse->last_mouse_event[mac_button][release] = mach_absolute_time(); -} + input_t + input() { + input_t result { new macos_input_t() }; -void scroll(input_t &input, int high_res_distance) { - CGEventRef upEvent = CGEventCreateScrollWheelEvent( - NULL, - kCGScrollEventUnitLine, - 2, high_res_distance > 0 ? 1 : -1, high_res_distance); - CGEventPost(kCGHIDEventTap, upEvent); - CFRelease(upEvent); -} + auto macos_input = (macos_input_t *) result.get(); -void hscroll(input_t &input, int high_res_distance) { - // Unimplemented -} + // If we don't use the main display in the future, this has to be adapted + macos_input->display = CGMainDisplayID(); -input_t input() { - input_t result { new macos_input_t() }; + // Input coordinates are based on the virtual resolution not the physical, so we need the scaling factor + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(macos_input->display); + macos_input->displayScaling = ((CGFloat) CGDisplayPixelsWide(macos_input->display)) / ((CGFloat) CGDisplayModeGetPixelWidth(mode)); + CFRelease(mode); - auto macos_input = (macos_input_t *)result.get(); + macos_input->source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); - // If we don't use the main display in the future, this has to be adapted - macos_input->display = CGMainDisplayID(); + macos_input->kb_event = CGEventCreate(macos_input->source); + macos_input->kb_flags = 0; - // Input coordinates are based on the virtual resolution not the physical, so we need the scaling factor - CGDisplayModeRef mode = CGDisplayCopyDisplayMode(macos_input->display); - macos_input->displayScaling = ((CGFloat)CGDisplayPixelsWide(macos_input->display)) / ((CGFloat)CGDisplayModeGetPixelWidth(mode)); - CFRelease(mode); + macos_input->mouse_event = CGEventCreate(macos_input->source); + macos_input->mouse_down[0] = false; + macos_input->mouse_down[1] = false; + macos_input->mouse_down[2] = false; + macos_input->last_mouse_event[0][0] = 0; + macos_input->last_mouse_event[0][1] = 0; + macos_input->last_mouse_event[1][0] = 0; + macos_input->last_mouse_event[1][1] = 0; + macos_input->last_mouse_event[2][0] = 0; + macos_input->last_mouse_event[2][1] = 0; - macos_input->source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); + BOOST_LOG(debug) << "Display "sv << macos_input->display << ", pixel dimention: " << CGDisplayPixelsWide(macos_input->display) << "x"sv << CGDisplayPixelsHigh(macos_input->display); - macos_input->kb_event = CGEventCreate(macos_input->source); - macos_input->kb_flags = 0; + return result; + } - macos_input->mouse_event = CGEventCreate(macos_input->source); - macos_input->mouse_down[0] = false; - macos_input->mouse_down[1] = false; - macos_input->mouse_down[2] = false; - macos_input->last_mouse_event[0][0] = 0; - macos_input->last_mouse_event[0][1] = 0; - macos_input->last_mouse_event[1][0] = 0; - macos_input->last_mouse_event[1][1] = 0; - macos_input->last_mouse_event[2][0] = 0; - macos_input->last_mouse_event[2][1] = 0; + void + freeInput(void *p) { + auto *input = (macos_input_t *) p; - BOOST_LOG(debug) << "Display "sv << macos_input->display << ", pixel dimention: " << CGDisplayPixelsWide(macos_input->display) << "x"sv << CGDisplayPixelsHigh(macos_input->display); + CFRelease(input->source); + CFRelease(input->kb_event); + CFRelease(input->mouse_event); - return result; -} + delete input; + } -void freeInput(void *p) { - auto *input = (macos_input_t *)p; + std::vector & + supported_gamepads() { + static std::vector gamepads { ""sv }; - CFRelease(input->source); - CFRelease(input->kb_event); - CFRelease(input->mouse_event); - - delete input; -} - -std::vector &supported_gamepads() { - static std::vector gamepads { ""sv }; - - return gamepads; -} -} // namespace platf + return gamepads; + } +} // namespace platf diff --git a/src/platform/macos/microphone.mm b/src/platform/macos/microphone.mm index 6e367e9e..df40db9d 100644 --- a/src/platform/macos/microphone.mm +++ b/src/platform/macos/microphone.mm @@ -5,83 +5,88 @@ #include "src/main.h" namespace platf { -using namespace std::literals; + using namespace std::literals; -struct av_mic_t : public mic_t { - AVAudio *av_audio_capture; + struct av_mic_t: public mic_t { + AVAudio *av_audio_capture; - ~av_mic_t() { - [av_audio_capture release]; - } - - capture_e sample(std::vector &sample_in) override { - auto sample_size = sample_in.size(); - - uint32_t length = 0; - void *byteSampleBuffer = TPCircularBufferTail(&av_audio_capture->audioSampleBuffer, &length); - - while(length < sample_size * sizeof(std::int16_t)) { - [av_audio_capture.samplesArrivedSignal wait]; - byteSampleBuffer = TPCircularBufferTail(&av_audio_capture->audioSampleBuffer, &length); + ~av_mic_t() { + [av_audio_capture release]; } - const int16_t *sampleBuffer = (int16_t *)byteSampleBuffer; - std::vector vectorBuffer(sampleBuffer, sampleBuffer + sample_size); + capture_e + sample(std::vector &sample_in) override { + auto sample_size = sample_in.size(); - std::copy_n(std::begin(vectorBuffer), sample_size, std::begin(sample_in)); + uint32_t length = 0; + void *byteSampleBuffer = TPCircularBufferTail(&av_audio_capture->audioSampleBuffer, &length); - TPCircularBufferConsume(&av_audio_capture->audioSampleBuffer, sample_size * sizeof(std::int16_t)); - - return capture_e::ok; - } -}; - -struct macos_audio_control_t : public audio_control_t { - AVCaptureDevice *audio_capture_device; - -public: - int set_sink(const std::string &sink) override { - BOOST_LOG(warning) << "audio_control_t::set_sink() unimplemented: "sv << sink; - return 0; - } - - std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override { - auto mic = std::make_unique(); - const char *audio_sink = ""; - - if(!config::audio.sink.empty()) { - audio_sink = config::audio.sink.c_str(); - } - - if((audio_capture_device = [AVAudio findMicrophone:[NSString stringWithUTF8String:audio_sink]]) == nullptr) { - BOOST_LOG(error) << "opening microphone '"sv << audio_sink << "' failed. Please set a valid input source in the Sunshine config."sv; - BOOST_LOG(error) << "Available inputs:"sv; - - for(NSString *name in [AVAudio microphoneNames]) { - BOOST_LOG(error) << "\t"sv << [name UTF8String]; + while (length < sample_size * sizeof(std::int16_t)) { + [av_audio_capture.samplesArrivedSignal wait]; + byteSampleBuffer = TPCircularBufferTail(&av_audio_capture->audioSampleBuffer, &length); } - return nullptr; + const int16_t *sampleBuffer = (int16_t *) byteSampleBuffer; + std::vector vectorBuffer(sampleBuffer, sampleBuffer + sample_size); + + std::copy_n(std::begin(vectorBuffer), sample_size, std::begin(sample_in)); + + TPCircularBufferConsume(&av_audio_capture->audioSampleBuffer, sample_size * sizeof(std::int16_t)); + + return capture_e::ok; + } + }; + + struct macos_audio_control_t: public audio_control_t { + AVCaptureDevice *audio_capture_device; + + public: + int + set_sink(const std::string &sink) override { + BOOST_LOG(warning) << "audio_control_t::set_sink() unimplemented: "sv << sink; + return 0; } - mic->av_audio_capture = [[AVAudio alloc] init]; + std::unique_ptr + microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override { + auto mic = std::make_unique(); + const char *audio_sink = ""; - if([mic->av_audio_capture setupMicrophone:audio_capture_device sampleRate:sample_rate frameSize:frame_size channels:channels]) { - BOOST_LOG(error) << "Failed to setup microphone."sv; - return nullptr; + if (!config::audio.sink.empty()) { + audio_sink = config::audio.sink.c_str(); + } + + if ((audio_capture_device = [AVAudio findMicrophone:[NSString stringWithUTF8String:audio_sink]]) == nullptr) { + BOOST_LOG(error) << "opening microphone '"sv << audio_sink << "' failed. Please set a valid input source in the Sunshine config."sv; + BOOST_LOG(error) << "Available inputs:"sv; + + for (NSString *name in [AVAudio microphoneNames]) { + BOOST_LOG(error) << "\t"sv << [name UTF8String]; + } + + return nullptr; + } + + mic->av_audio_capture = [[AVAudio alloc] init]; + + if ([mic->av_audio_capture setupMicrophone:audio_capture_device sampleRate:sample_rate frameSize:frame_size channels:channels]) { + BOOST_LOG(error) << "Failed to setup microphone."sv; + return nullptr; + } + + return mic; } - return mic; + std::optional + sink_info() override { + sink_t sink; + + return sink; + } + }; + + std::unique_ptr + audio_control() { + return std::make_unique(); } - - std::optional sink_info() override { - sink_t sink; - - return sink; - } -}; - -std::unique_ptr audio_control() { - return std::make_unique(); -} -} // namespace platf +} // namespace platf diff --git a/src/platform/macos/misc.h b/src/platform/macos/misc.h index f0d04a33..10b89bf4 100644 --- a/src/platform/macos/misc.h +++ b/src/platform/macos/misc.h @@ -6,11 +6,13 @@ #include namespace dyn { -typedef void (*apiproc)(void); + typedef void (*apiproc)(void); -int load(void *handle, const std::vector> &funcs, bool strict = true); -void *handle(const std::vector &libs); + int + load(void *handle, const std::vector> &funcs, bool strict = true); + void * + handle(const std::vector &libs); -} // namespace dyn +} // namespace dyn #endif diff --git a/src/platform/macos/misc.mm b/src/platform/macos/misc.mm index f9767aef..3d863729 100644 --- a/src/platform/macos/misc.mm +++ b/src/platform/macos/misc.mm @@ -20,229 +20,247 @@ namespace platf { // Even though the following two functions are available starting in macOS 10.15, they weren't // actually in the Mac SDK until Xcode 12.2, the first to include the SDK for macOS 11 -#if __MAC_OS_X_VERSION_MAX_ALLOWED < 110000 // __MAC_11_0 -// If they're not in the SDK then we can use our own function definitions. -// Need to use weak import so that this will link in macOS 10.14 and earlier -extern "C" bool CGPreflightScreenCaptureAccess(void) __attribute__((weak_import)); -extern "C" bool CGRequestScreenCaptureAccess(void) __attribute__((weak_import)); +#if __MAC_OS_X_VERSION_MAX_ALLOWED < 110000 // __MAC_11_0 + // If they're not in the SDK then we can use our own function definitions. + // Need to use weak import so that this will link in macOS 10.14 and earlier + extern "C" bool + CGPreflightScreenCaptureAccess(void) __attribute__((weak_import)); + extern "C" bool + CGRequestScreenCaptureAccess(void) __attribute__((weak_import)); #endif -std::unique_ptr init() { - // This will generate a warning about CGPreflightScreenCaptureAccess and - // CGRequestScreenCaptureAccess being unavailable before macOS 10.15, but - // we have a guard to prevent it from being called on those earlier systems. - // Unfortunately the supported way to silence this warning, using @available, - // produces linker errors for __isPlatformVersionAtLeast, so we have to use - // a different method. - // We also ignore "tautological-pointer-compare" because when compiling with - // Xcode 12.2 and later, these functions are not weakly linked and will never - // be null, and therefore generate this warning. Since we are weakly linking - // when compiling with earlier Xcode versions, the check for null is - // necessary and so we ignore the warning. + std::unique_ptr + init() { + // This will generate a warning about CGPreflightScreenCaptureAccess and + // CGRequestScreenCaptureAccess being unavailable before macOS 10.15, but + // we have a guard to prevent it from being called on those earlier systems. + // Unfortunately the supported way to silence this warning, using @available, + // produces linker errors for __isPlatformVersionAtLeast, so we have to use + // a different method. + // We also ignore "tautological-pointer-compare" because when compiling with + // Xcode 12.2 and later, these functions are not weakly linked and will never + // be null, and therefore generate this warning. Since we are weakly linking + // when compiling with earlier Xcode versions, the check for null is + // necessary and so we ignore the warning. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability-new" #pragma clang diagnostic ignored "-Wtautological-pointer-compare" - if([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:((NSOperatingSystemVersion) { 10, 15, 0 })] && - // Double check that these weakly-linked symbols have been loaded: - CGPreflightScreenCaptureAccess != nullptr && CGRequestScreenCaptureAccess != nullptr && - !CGPreflightScreenCaptureAccess()) { - BOOST_LOG(error) << "No screen capture permission!"sv; - BOOST_LOG(error) << "Please activate it in 'System Preferences' -> 'Privacy' -> 'Screen Recording'"sv; - CGRequestScreenCaptureAccess(); - return nullptr; - } + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:((NSOperatingSystemVersion) { 10, 15, 0 })] && + // Double check that these weakly-linked symbols have been loaded: + CGPreflightScreenCaptureAccess != nullptr && CGRequestScreenCaptureAccess != nullptr && + !CGPreflightScreenCaptureAccess()) { + BOOST_LOG(error) << "No screen capture permission!"sv; + BOOST_LOG(error) << "Please activate it in 'System Preferences' -> 'Privacy' -> 'Screen Recording'"sv; + CGRequestScreenCaptureAccess(); + return nullptr; + } #pragma clang diagnostic pop - return std::make_unique(); -} - -fs::path appdata() { - const char *homedir; - if((homedir = getenv("HOME")) == nullptr) { - homedir = getpwuid(geteuid())->pw_dir; + return std::make_unique(); } - return fs::path { homedir } / ".config/sunshine"sv; -} + fs::path + appdata() { + const char *homedir; + if ((homedir = getenv("HOME")) == nullptr) { + homedir = getpwuid(geteuid())->pw_dir; + } -using ifaddr_t = util::safe_ptr; - -ifaddr_t get_ifaddrs() { - ifaddrs *p { nullptr }; - - getifaddrs(&p); - - return ifaddr_t { p }; -} - -std::string from_sockaddr(const sockaddr *const ip_addr) { - char data[INET6_ADDRSTRLEN]; - - auto family = ip_addr->sa_family; - if(family == AF_INET6) { - inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data, - INET6_ADDRSTRLEN); + return fs::path { homedir } / ".config/sunshine"sv; } - if(family == AF_INET) { - inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data, - INET_ADDRSTRLEN); + using ifaddr_t = util::safe_ptr; + + ifaddr_t + get_ifaddrs() { + ifaddrs *p { nullptr }; + + getifaddrs(&p); + + return ifaddr_t { p }; } - return std::string { data }; -} + std::string + from_sockaddr(const sockaddr *const ip_addr) { + char data[INET6_ADDRSTRLEN]; -std::pair from_sockaddr_ex(const sockaddr *const ip_addr) { - char data[INET6_ADDRSTRLEN]; + auto family = ip_addr->sa_family; + if (family == AF_INET6) { + inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, + INET6_ADDRSTRLEN); + } - auto family = ip_addr->sa_family; - std::uint16_t port; - if(family == AF_INET6) { - inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data, - INET6_ADDRSTRLEN); - port = ((sockaddr_in6 *)ip_addr)->sin6_port; + if (family == AF_INET) { + inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, + INET_ADDRSTRLEN); + } + + return std::string { data }; } - if(family == AF_INET) { - inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data, - INET_ADDRSTRLEN); - port = ((sockaddr_in *)ip_addr)->sin_port; + std::pair + from_sockaddr_ex(const sockaddr *const ip_addr) { + char data[INET6_ADDRSTRLEN]; + + auto family = ip_addr->sa_family; + std::uint16_t port; + if (family == AF_INET6) { + inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, + INET6_ADDRSTRLEN); + port = ((sockaddr_in6 *) ip_addr)->sin6_port; + } + + if (family == AF_INET) { + inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, + INET_ADDRSTRLEN); + port = ((sockaddr_in *) ip_addr)->sin_port; + } + + return { port, std::string { data } }; } - return { port, std::string { data } }; -} + std::string + get_mac_address(const std::string_view &address) { + auto ifaddrs = get_ifaddrs(); -std::string get_mac_address(const std::string_view &address) { - auto ifaddrs = get_ifaddrs(); + for (auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) { + if (pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) { + BOOST_LOG(verbose) << "Looking for MAC of "sv << pos->ifa_name; - for(auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) { - if(pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) { - BOOST_LOG(verbose) << "Looking for MAC of "sv << pos->ifa_name; + struct ifaddrs *ifap, *ifaptr; + unsigned char *ptr; + std::string mac_address; - struct ifaddrs *ifap, *ifaptr; - unsigned char *ptr; - std::string mac_address; + if (getifaddrs(&ifap) == 0) { + for (ifaptr = ifap; ifaptr != NULL; ifaptr = (ifaptr)->ifa_next) { + if (!strcmp((ifaptr)->ifa_name, pos->ifa_name) && (((ifaptr)->ifa_addr)->sa_family == AF_LINK)) { + ptr = (unsigned char *) LLADDR((struct sockaddr_dl *) (ifaptr)->ifa_addr); + char buff[100]; - if(getifaddrs(&ifap) == 0) { - for(ifaptr = ifap; ifaptr != NULL; ifaptr = (ifaptr)->ifa_next) { - if(!strcmp((ifaptr)->ifa_name, pos->ifa_name) && (((ifaptr)->ifa_addr)->sa_family == AF_LINK)) { - ptr = (unsigned char *)LLADDR((struct sockaddr_dl *)(ifaptr)->ifa_addr); - char buff[100]; + snprintf(buff, sizeof(buff), "%02x:%02x:%02x:%02x:%02x:%02x", + *ptr, *(ptr + 1), *(ptr + 2), *(ptr + 3), *(ptr + 4), *(ptr + 5)); + mac_address = buff; + break; + } + } - snprintf(buff, sizeof(buff), "%02x:%02x:%02x:%02x:%02x:%02x", - *ptr, *(ptr + 1), *(ptr + 2), *(ptr + 3), *(ptr + 4), *(ptr + 5)); - mac_address = buff; - break; + freeifaddrs(ifap); + + if (ifaptr != NULL) { + BOOST_LOG(verbose) << "Found MAC of "sv << pos->ifa_name << ": "sv << mac_address; + return mac_address; } } + } + } - freeifaddrs(ifap); + BOOST_LOG(warning) << "Unable to find MAC address for "sv << address; + return "00:00:00:00:00:00"s; + } - if(ifaptr != NULL) { - BOOST_LOG(verbose) << "Found MAC of "sv << pos->ifa_name << ": "sv << mac_address; - return mac_address; - } + bp::child + run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { + BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv; + if (!group) { + if (!file) { + return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); + } + else { + return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec); + } + } + else { + if (!file) { + return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec, *group); + } + else { + return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec, *group); } } } - BOOST_LOG(warning) << "Unable to find MAC address for "sv << address; - return "00:00:00:00:00:00"s; -} - -bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv; - if(!group) { - if(!file) { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); - } - else { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec); - } + void + adjust_thread_priority(thread_priority_e priority) { + // Unimplemented } - else { - if(!file) { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec, *group); - } - else { - return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec, *group); - } + + void + streaming_will_start() { + // Nothing to do } -} -void adjust_thread_priority(thread_priority_e priority) { - // Unimplemented -} + void + streaming_will_stop() { + // Nothing to do + } -void streaming_will_start() { - // Nothing to do -} + bool + restart_supported() { + // Restart not supported yet + return false; + } -void streaming_will_stop() { - // Nothing to do -} + bool + restart() { + // Restart not supported yet + return false; + } -bool restart_supported() { - // Restart not supported yet - return false; -} + bool + send_batch(batched_send_info_t &send_info) { + // Fall back to unbatched send calls + return false; + } -bool restart() { - // Restart not supported yet - return false; -} + std::unique_ptr + enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { + // Unimplemented + // + // NB: When implementing, remember to consider that some routes can drop DSCP-tagged packets completely! + return nullptr; + } -bool send_batch(batched_send_info_t &send_info) { - // Fall back to unbatched send calls - return false; -} - -std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { - // Unimplemented - // - // NB: When implementing, remember to consider that some routes can drop DSCP-tagged packets completely! - return nullptr; -} - -} // namespace platf +} // namespace platf namespace dyn { -void *handle(const std::vector &libs) { - void *handle; + void * + handle(const std::vector &libs) { + void *handle; - for(auto lib : libs) { - handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL); - if(handle) { - return handle; + for (auto lib : libs) { + handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL); + if (handle) { + return handle; + } } + + std::stringstream ss; + ss << "Couldn't find any of the following libraries: ["sv << libs.front(); + std::for_each(std::begin(libs) + 1, std::end(libs), [&](auto lib) { + ss << ", "sv << lib; + }); + + ss << ']'; + + BOOST_LOG(error) << ss.str(); + + return nullptr; } - std::stringstream ss; - ss << "Couldn't find any of the following libraries: ["sv << libs.front(); - std::for_each(std::begin(libs) + 1, std::end(libs), [&](auto lib) { - ss << ", "sv << lib; - }); + int + load(void *handle, const std::vector> &funcs, bool strict) { + int err = 0; + for (auto &func : funcs) { + TUPLE_2D_REF(fn, name, func); - ss << ']'; + *fn = (void (*)()) dlsym(handle, name); - BOOST_LOG(error) << ss.str(); + if (!*fn && strict) { + BOOST_LOG(error) << "Couldn't find function: "sv << name; - return nullptr; -} - -int load(void *handle, const std::vector> &funcs, bool strict) { - int err = 0; - for(auto &func : funcs) { - TUPLE_2D_REF(fn, name, func); - - *fn = (void (*)())dlsym(handle, name); - - if(!*fn && strict) { - BOOST_LOG(error) << "Couldn't find function: "sv << name; - - err = -1; + err = -1; + } } - } - return err; -} -} // namespace dyn + return err; + } +} // namespace dyn diff --git a/src/platform/macos/nv12_zero_device.cpp b/src/platform/macos/nv12_zero_device.cpp index 71e58307..0c62328f 100644 --- a/src/platform/macos/nv12_zero_device.cpp +++ b/src/platform/macos/nv12_zero_device.cpp @@ -9,74 +9,79 @@ extern "C" { namespace platf { -void free_frame(AVFrame *frame) { - av_frame_free(&frame); -} + void + free_frame(AVFrame *frame) { + av_frame_free(&frame); + } -util::safe_ptr av_frame; + util::safe_ptr av_frame; -int nv12_zero_device::convert(platf::img_t &img) { - av_frame_make_writable(av_frame.get()); + int + nv12_zero_device::convert(platf::img_t &img) { + av_frame_make_writable(av_frame.get()); - av_img_t *av_img = (av_img_t *)&img; + av_img_t *av_img = (av_img_t *) &img; - size_t left_pad, right_pad, top_pad, bottom_pad; - CVPixelBufferGetExtendedPixels(av_img->pixel_buffer, &left_pad, &right_pad, &top_pad, &bottom_pad); + size_t left_pad, right_pad, top_pad, bottom_pad; + CVPixelBufferGetExtendedPixels(av_img->pixel_buffer, &left_pad, &right_pad, &top_pad, &bottom_pad); - const uint8_t *data = (const uint8_t *)CVPixelBufferGetBaseAddressOfPlane(av_img->pixel_buffer, 0) - left_pad - (top_pad * img.width); + const uint8_t *data = (const uint8_t *) CVPixelBufferGetBaseAddressOfPlane(av_img->pixel_buffer, 0) - left_pad - (top_pad * img.width); - int result = av_image_fill_arrays(av_frame->data, av_frame->linesize, data, (AVPixelFormat)av_frame->format, img.width, img.height, 32); + int result = av_image_fill_arrays(av_frame->data, av_frame->linesize, data, (AVPixelFormat) av_frame->format, img.width, img.height, 32); - // We will create the black bars for the padding top/bottom or left/right here in very cheap way. - // The luminance is 0, therefore, we simply need to set the chroma values to 128 for each pixel - // for black bars (instead of green with chroma 0). However, this only works 100% correct, when - // the resolution is devisable by 32. This could be improved by calculating the chroma values for - // the outer content pixels, which should introduce only a minor performance hit. - // - // XXX: Improve the algorithm to take into account the outer pixels + // We will create the black bars for the padding top/bottom or left/right here in very cheap way. + // The luminance is 0, therefore, we simply need to set the chroma values to 128 for each pixel + // for black bars (instead of green with chroma 0). However, this only works 100% correct, when + // the resolution is devisable by 32. This could be improved by calculating the chroma values for + // the outer content pixels, which should introduce only a minor performance hit. + // + // XXX: Improve the algorithm to take into account the outer pixels - size_t uv_plane_height = CVPixelBufferGetHeightOfPlane(av_img->pixel_buffer, 1); + size_t uv_plane_height = CVPixelBufferGetHeightOfPlane(av_img->pixel_buffer, 1); - if(left_pad || right_pad) { - for(int l = 0; l < uv_plane_height + (top_pad / 2); l++) { - int line = l * av_frame->linesize[1]; - memset((void *)&av_frame->data[1][line], 128, (size_t)left_pad); - memset((void *)&av_frame->data[1][line + img.width - right_pad], 128, right_pad); + if (left_pad || right_pad) { + for (int l = 0; l < uv_plane_height + (top_pad / 2); l++) { + int line = l * av_frame->linesize[1]; + memset((void *) &av_frame->data[1][line], 128, (size_t) left_pad); + memset((void *) &av_frame->data[1][line + img.width - right_pad], 128, right_pad); + } } + + if (top_pad || bottom_pad) { + memset((void *) &av_frame->data[1][0], 128, (top_pad / 2) * av_frame->linesize[1]); + memset((void *) &av_frame->data[1][((top_pad / 2) + uv_plane_height) * av_frame->linesize[1]], 128, bottom_pad / 2 * av_frame->linesize[1]); + } + + return result > 0 ? 0 : -1; } - if(top_pad || bottom_pad) { - memset((void *)&av_frame->data[1][0], 128, (top_pad / 2) * av_frame->linesize[1]); - memset((void *)&av_frame->data[1][((top_pad / 2) + uv_plane_height) * av_frame->linesize[1]], 128, bottom_pad / 2 * av_frame->linesize[1]); + int + nv12_zero_device::set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { + this->frame = frame; + + av_frame.reset(frame); + + resolution_fn(this->display, frame->width, frame->height); + + return 0; } - return result > 0 ? 0 : -1; -} + void + nv12_zero_device::set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) { + } -int nv12_zero_device::set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) { - this->frame = frame; + int + nv12_zero_device::init(void *display, resolution_fn_t resolution_fn, pixel_format_fn_t pixel_format_fn) { + pixel_format_fn(display, '420v'); - av_frame.reset(frame); + this->display = display; + this->resolution_fn = resolution_fn; - resolution_fn(this->display, frame->width, frame->height); + // we never use this pointer but it's existence is checked/used + // by the platform independed code + data = this; - return 0; -} + return 0; + } -void nv12_zero_device::set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) { -} - -int nv12_zero_device::init(void *display, resolution_fn_t resolution_fn, pixel_format_fn_t pixel_format_fn) { - pixel_format_fn(display, '420v'); - - this->display = display; - this->resolution_fn = resolution_fn; - - // we never use this pointer but it's existence is checked/used - // by the platform independed code - data = this; - - return 0; -} - -} // namespace platf +} // namespace platf diff --git a/src/platform/macos/nv12_zero_device.h b/src/platform/macos/nv12_zero_device.h index 1863fb0f..53b6eff1 100644 --- a/src/platform/macos/nv12_zero_device.h +++ b/src/platform/macos/nv12_zero_device.h @@ -5,25 +5,29 @@ namespace platf { -class nv12_zero_device : public hwdevice_t { - // display holds a pointer to an av_video object. Since the namespaces of AVFoundation - // and FFMPEG collide, we need this opaque pointer and cannot use the definition - void *display; + class nv12_zero_device: public hwdevice_t { + // display holds a pointer to an av_video object. Since the namespaces of AVFoundation + // and FFMPEG collide, we need this opaque pointer and cannot use the definition + void *display; -public: - // this function is used to set the resolution on an av_video object that we cannot - // call directly because of namespace collisions between AVFoundation and FFMPEG - using resolution_fn_t = std::function; - resolution_fn_t resolution_fn; - using pixel_format_fn_t = std::function; + public: + // this function is used to set the resolution on an av_video object that we cannot + // call directly because of namespace collisions between AVFoundation and FFMPEG + using resolution_fn_t = std::function; + resolution_fn_t resolution_fn; + using pixel_format_fn_t = std::function; - int init(void *display, resolution_fn_t resolution_fn, pixel_format_fn_t pixel_format_fn); + int + init(void *display, resolution_fn_t resolution_fn, pixel_format_fn_t pixel_format_fn); - int convert(img_t &img); - int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx); - void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range); -}; + int + convert(img_t &img); + int + set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx); + void + set_colorspace(std::uint32_t colorspace, std::uint32_t color_range); + }; -} // namespace platf +} // namespace platf #endif /* vtdevice_h */ diff --git a/src/platform/macos/publish.cpp b/src/platform/macos/publish.cpp index bd943ec9..c22d991d 100644 --- a/src/platform/macos/publish.cpp +++ b/src/platform/macos/publish.cpp @@ -12,418 +12,426 @@ using namespace std::literals; namespace avahi { -/** Error codes used by avahi */ -enum err_e { - OK = 0, /**< OK */ - ERR_FAILURE = -1, /**< Generic error code */ - ERR_BAD_STATE = -2, /**< Object was in a bad state */ - ERR_INVALID_HOST_NAME = -3, /**< Invalid host name */ - ERR_INVALID_DOMAIN_NAME = -4, /**< Invalid domain name */ - ERR_NO_NETWORK = -5, /**< No suitable network protocol available */ - ERR_INVALID_TTL = -6, /**< Invalid DNS TTL */ - ERR_IS_PATTERN = -7, /**< RR key is pattern */ - ERR_COLLISION = -8, /**< Name collision */ - ERR_INVALID_RECORD = -9, /**< Invalid RR */ + /** Error codes used by avahi */ + enum err_e { + OK = 0, /**< OK */ + ERR_FAILURE = -1, /**< Generic error code */ + ERR_BAD_STATE = -2, /**< Object was in a bad state */ + ERR_INVALID_HOST_NAME = -3, /**< Invalid host name */ + ERR_INVALID_DOMAIN_NAME = -4, /**< Invalid domain name */ + ERR_NO_NETWORK = -5, /**< No suitable network protocol available */ + ERR_INVALID_TTL = -6, /**< Invalid DNS TTL */ + ERR_IS_PATTERN = -7, /**< RR key is pattern */ + ERR_COLLISION = -8, /**< Name collision */ + ERR_INVALID_RECORD = -9, /**< Invalid RR */ - ERR_INVALID_SERVICE_NAME = -10, /**< Invalid service name */ - ERR_INVALID_SERVICE_TYPE = -11, /**< Invalid service type */ - ERR_INVALID_PORT = -12, /**< Invalid port number */ - ERR_INVALID_KEY = -13, /**< Invalid key */ - ERR_INVALID_ADDRESS = -14, /**< Invalid address */ - ERR_TIMEOUT = -15, /**< Timeout reached */ - ERR_TOO_MANY_CLIENTS = -16, /**< Too many clients */ - ERR_TOO_MANY_OBJECTS = -17, /**< Too many objects */ - ERR_TOO_MANY_ENTRIES = -18, /**< Too many entries */ - ERR_OS = -19, /**< OS error */ + ERR_INVALID_SERVICE_NAME = -10, /**< Invalid service name */ + ERR_INVALID_SERVICE_TYPE = -11, /**< Invalid service type */ + ERR_INVALID_PORT = -12, /**< Invalid port number */ + ERR_INVALID_KEY = -13, /**< Invalid key */ + ERR_INVALID_ADDRESS = -14, /**< Invalid address */ + ERR_TIMEOUT = -15, /**< Timeout reached */ + ERR_TOO_MANY_CLIENTS = -16, /**< Too many clients */ + ERR_TOO_MANY_OBJECTS = -17, /**< Too many objects */ + ERR_TOO_MANY_ENTRIES = -18, /**< Too many entries */ + ERR_OS = -19, /**< OS error */ - ERR_ACCESS_DENIED = -20, /**< Access denied */ - ERR_INVALID_OPERATION = -21, /**< Invalid operation */ - ERR_DBUS_ERROR = -22, /**< An unexpected D-Bus error occurred */ - ERR_DISCONNECTED = -23, /**< Daemon connection failed */ - ERR_NO_MEMORY = -24, /**< Memory exhausted */ - ERR_INVALID_OBJECT = -25, /**< The object passed to this function was invalid */ - ERR_NO_DAEMON = -26, /**< Daemon not running */ - ERR_INVALID_INTERFACE = -27, /**< Invalid interface */ - ERR_INVALID_PROTOCOL = -28, /**< Invalid protocol */ - ERR_INVALID_FLAGS = -29, /**< Invalid flags */ + ERR_ACCESS_DENIED = -20, /**< Access denied */ + ERR_INVALID_OPERATION = -21, /**< Invalid operation */ + ERR_DBUS_ERROR = -22, /**< An unexpected D-Bus error occurred */ + ERR_DISCONNECTED = -23, /**< Daemon connection failed */ + ERR_NO_MEMORY = -24, /**< Memory exhausted */ + ERR_INVALID_OBJECT = -25, /**< The object passed to this function was invalid */ + ERR_NO_DAEMON = -26, /**< Daemon not running */ + ERR_INVALID_INTERFACE = -27, /**< Invalid interface */ + ERR_INVALID_PROTOCOL = -28, /**< Invalid protocol */ + ERR_INVALID_FLAGS = -29, /**< Invalid flags */ - ERR_NOT_FOUND = -30, /**< Not found */ - ERR_INVALID_CONFIG = -31, /**< Configuration error */ - ERR_VERSION_MISMATCH = -32, /**< Verson mismatch */ - ERR_INVALID_SERVICE_SUBTYPE = -33, /**< Invalid service subtype */ - ERR_INVALID_PACKET = -34, /**< Invalid packet */ - ERR_INVALID_DNS_ERROR = -35, /**< Invlaid DNS return code */ - ERR_DNS_FORMERR = -36, /**< DNS Error: Form error */ - ERR_DNS_SERVFAIL = -37, /**< DNS Error: Server Failure */ - ERR_DNS_NXDOMAIN = -38, /**< DNS Error: No such domain */ - ERR_DNS_NOTIMP = -39, /**< DNS Error: Not implemented */ + ERR_NOT_FOUND = -30, /**< Not found */ + ERR_INVALID_CONFIG = -31, /**< Configuration error */ + ERR_VERSION_MISMATCH = -32, /**< Verson mismatch */ + ERR_INVALID_SERVICE_SUBTYPE = -33, /**< Invalid service subtype */ + ERR_INVALID_PACKET = -34, /**< Invalid packet */ + ERR_INVALID_DNS_ERROR = -35, /**< Invlaid DNS return code */ + ERR_DNS_FORMERR = -36, /**< DNS Error: Form error */ + ERR_DNS_SERVFAIL = -37, /**< DNS Error: Server Failure */ + ERR_DNS_NXDOMAIN = -38, /**< DNS Error: No such domain */ + ERR_DNS_NOTIMP = -39, /**< DNS Error: Not implemented */ - ERR_DNS_REFUSED = -40, /**< DNS Error: Operation refused */ - ERR_DNS_YXDOMAIN = -41, - ERR_DNS_YXRRSET = -42, - ERR_DNS_NXRRSET = -43, - ERR_DNS_NOTAUTH = -44, /**< DNS Error: Not authorized */ - ERR_DNS_NOTZONE = -45, - ERR_INVALID_RDATA = -46, /**< Invalid RDATA */ - ERR_INVALID_DNS_CLASS = -47, /**< Invalid DNS class */ - ERR_INVALID_DNS_TYPE = -48, /**< Invalid DNS type */ - ERR_NOT_SUPPORTED = -49, /**< Not supported */ + ERR_DNS_REFUSED = -40, /**< DNS Error: Operation refused */ + ERR_DNS_YXDOMAIN = -41, + ERR_DNS_YXRRSET = -42, + ERR_DNS_NXRRSET = -43, + ERR_DNS_NOTAUTH = -44, /**< DNS Error: Not authorized */ + ERR_DNS_NOTZONE = -45, + ERR_INVALID_RDATA = -46, /**< Invalid RDATA */ + ERR_INVALID_DNS_CLASS = -47, /**< Invalid DNS class */ + ERR_INVALID_DNS_TYPE = -48, /**< Invalid DNS type */ + ERR_NOT_SUPPORTED = -49, /**< Not supported */ - ERR_NOT_PERMITTED = -50, /**< Operation not permitted */ - ERR_INVALID_ARGUMENT = -51, /**< Invalid argument */ - ERR_IS_EMPTY = -52, /**< Is empty */ - ERR_NO_CHANGE = -53, /**< The requested operation is invalid because it is redundant */ + ERR_NOT_PERMITTED = -50, /**< Operation not permitted */ + ERR_INVALID_ARGUMENT = -51, /**< Invalid argument */ + ERR_IS_EMPTY = -52, /**< Is empty */ + ERR_NO_CHANGE = -53, /**< The requested operation is invalid because it is redundant */ - ERR_MAX = -54 -}; - -constexpr auto IF_UNSPEC = -1; -enum proto { - PROTO_INET = 0, /**< IPv4 */ - PROTO_INET6 = 1, /**< IPv6 */ - PROTO_UNSPEC = -1 /**< Unspecified/all protocol(s) */ -}; - -enum ServerState { - SERVER_INVALID, /**< Invalid state (initial) */ - SERVER_REGISTERING, /**< Host RRs are being registered */ - SERVER_RUNNING, /**< All host RRs have been established */ - SERVER_COLLISION, /**< There is a collision with a host RR. All host RRs have been withdrawn, the user should set a new host name via avahi_server_set_host_name() */ - SERVER_FAILURE /**< Some fatal failure happened, the server is unable to proceed */ -}; - -enum ClientState { - CLIENT_S_REGISTERING = SERVER_REGISTERING, /**< Server state: REGISTERING */ - CLIENT_S_RUNNING = SERVER_RUNNING, /**< Server state: RUNNING */ - CLIENT_S_COLLISION = SERVER_COLLISION, /**< Server state: COLLISION */ - CLIENT_FAILURE = 100, /**< Some kind of error happened on the client side */ - CLIENT_CONNECTING = 101 /**< We're still connecting. This state is only entered when AVAHI_CLIENT_NO_FAIL has been passed to avahi_client_new() and the daemon is not yet available. */ -}; - -enum EntryGroupState { - ENTRY_GROUP_UNCOMMITED, /**< The group has not yet been commited, the user must still call avahi_entry_group_commit() */ - ENTRY_GROUP_REGISTERING, /**< The entries of the group are currently being registered */ - ENTRY_GROUP_ESTABLISHED, /**< The entries have successfully been established */ - ENTRY_GROUP_COLLISION, /**< A name collision for one of the entries in the group has been detected, the entries have been withdrawn */ - ENTRY_GROUP_FAILURE /**< Some kind of failure happened, the entries have been withdrawn */ -}; - -enum ClientFlags { - CLIENT_IGNORE_USER_CONFIG = 1, /**< Don't read user configuration */ - CLIENT_NO_FAIL = 2 /**< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear */ -}; - -/** Some flags for publishing functions */ -enum PublishFlags { - PUBLISH_UNIQUE = 1, /**< For raw records: The RRset is intended to be unique */ - PUBLISH_NO_PROBE = 2, /**< For raw records: Though the RRset is intended to be unique no probes shall be sent */ - PUBLISH_NO_ANNOUNCE = 4, /**< For raw records: Do not announce this RR to other hosts */ - PUBLISH_ALLOW_MULTIPLE = 8, /**< For raw records: Allow multiple local records of this type, even if they are intended to be unique */ - /** \cond fulldocs */ - PUBLISH_NO_REVERSE = 16, /**< For address records: don't create a reverse (PTR) entry */ - PUBLISH_NO_COOKIE = 32, /**< For service records: do not implicitly add the local service cookie to TXT data */ - /** \endcond */ - PUBLISH_UPDATE = 64, /**< Update existing records instead of adding new ones */ - /** \cond fulldocs */ - PUBLISH_USE_WIDE_AREA = 128, /**< Register the record using wide area DNS (i.e. unicast DNS update) */ - PUBLISH_USE_MULTICAST = 256 /**< Register the record using multicast DNS */ - /** \endcond */ -}; - -using IfIndex = int; -using Protocol = int; - -struct EntryGroup; -struct Poll; -struct SimplePoll; -struct Client; - -typedef void (*ClientCallback)(Client *, ClientState, void *userdata); -typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata); - -typedef void (*free_fn)(void *); - -typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error); -typedef void (*client_free_fn)(Client *); -typedef char *(*alternative_service_name_fn)(char *); - -typedef Client *(*entry_group_get_client_fn)(EntryGroup *); - -typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata); -typedef int (*entry_group_add_service_fn)( - EntryGroup *group, - IfIndex interface, - Protocol protocol, - PublishFlags flags, - const char *name, - const char *type, - const char *domain, - const char *host, - uint16_t port, - ...); - -typedef int (*entry_group_is_empty_fn)(EntryGroup *); -typedef int (*entry_group_reset_fn)(EntryGroup *); -typedef int (*entry_group_commit_fn)(EntryGroup *); - -typedef char *(*strdup_fn)(const char *); -typedef char *(*strerror_fn)(int); -typedef int (*client_errno_fn)(Client *); - -typedef Poll *(*simple_poll_get_fn)(SimplePoll *); -typedef int (*simple_poll_loop_fn)(SimplePoll *); -typedef void (*simple_poll_quit_fn)(SimplePoll *); -typedef SimplePoll *(*simple_poll_new_fn)(); -typedef void (*simple_poll_free_fn)(SimplePoll *); - -free_fn free; -client_new_fn client_new; -client_free_fn client_free; -alternative_service_name_fn alternative_service_name; -entry_group_get_client_fn entry_group_get_client; -entry_group_new_fn entry_group_new; -entry_group_add_service_fn entry_group_add_service; -entry_group_is_empty_fn entry_group_is_empty; -entry_group_reset_fn entry_group_reset; -entry_group_commit_fn entry_group_commit; -strdup_fn strdup; -strerror_fn strerror; -client_errno_fn client_errno; -simple_poll_get_fn simple_poll_get; -simple_poll_loop_fn simple_poll_loop; -simple_poll_quit_fn simple_poll_quit; -simple_poll_new_fn simple_poll_new; -simple_poll_free_fn simple_poll_free; - - -int init_common() { - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if(funcs_loaded) return 0; - - if(!handle) { - handle = dyn::handle({ "libavahi-common.3.dylib", "libavahi-common.dylib" }); - if(!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *)&alternative_service_name, "avahi_alternative_service_name" }, - { (dyn::apiproc *)&free, "avahi_free" }, - { (dyn::apiproc *)&strdup, "avahi_strdup" }, - { (dyn::apiproc *)&strerror, "avahi_strerror" }, - { (dyn::apiproc *)&simple_poll_get, "avahi_simple_poll_get" }, - { (dyn::apiproc *)&simple_poll_loop, "avahi_simple_poll_loop" }, - { (dyn::apiproc *)&simple_poll_quit, "avahi_simple_poll_quit" }, - { (dyn::apiproc *)&simple_poll_new, "avahi_simple_poll_new" }, - { (dyn::apiproc *)&simple_poll_free, "avahi_simple_poll_free" }, + ERR_MAX = -54 }; - if(dyn::load(handle, funcs)) { - return -1; - } - - funcs_loaded = true; - return 0; -} - -int init_client() { - if(init_common()) { - return -1; - } - - static void *handle { nullptr }; - static bool funcs_loaded = false; - - if(funcs_loaded) return 0; - - if(!handle) { - handle = dyn::handle({ "libavahi-client.3.dylib", "libavahi-client.dylib" }); - if(!handle) { - return -1; - } - } - - std::vector> funcs { - { (dyn::apiproc *)&client_new, "avahi_client_new" }, - { (dyn::apiproc *)&client_free, "avahi_client_free" }, - { (dyn::apiproc *)&entry_group_get_client, "avahi_entry_group_get_client" }, - { (dyn::apiproc *)&entry_group_new, "avahi_entry_group_new" }, - { (dyn::apiproc *)&entry_group_add_service, "avahi_entry_group_add_service" }, - { (dyn::apiproc *)&entry_group_is_empty, "avahi_entry_group_is_empty" }, - { (dyn::apiproc *)&entry_group_reset, "avahi_entry_group_reset" }, - { (dyn::apiproc *)&entry_group_commit, "avahi_entry_group_commit" }, - { (dyn::apiproc *)&client_errno, "avahi_client_errno" }, + constexpr auto IF_UNSPEC = -1; + enum proto { + PROTO_INET = 0, /**< IPv4 */ + PROTO_INET6 = 1, /**< IPv6 */ + PROTO_UNSPEC = -1 /**< Unspecified/all protocol(s) */ }; - if(dyn::load(handle, funcs)) { - return -1; + enum ServerState { + SERVER_INVALID, /**< Invalid state (initial) */ + SERVER_REGISTERING, /**< Host RRs are being registered */ + SERVER_RUNNING, /**< All host RRs have been established */ + SERVER_COLLISION, /**< There is a collision with a host RR. All host RRs have been withdrawn, the user should set a new host name via avahi_server_set_host_name() */ + SERVER_FAILURE /**< Some fatal failure happened, the server is unable to proceed */ + }; + + enum ClientState { + CLIENT_S_REGISTERING = SERVER_REGISTERING, /**< Server state: REGISTERING */ + CLIENT_S_RUNNING = SERVER_RUNNING, /**< Server state: RUNNING */ + CLIENT_S_COLLISION = SERVER_COLLISION, /**< Server state: COLLISION */ + CLIENT_FAILURE = 100, /**< Some kind of error happened on the client side */ + CLIENT_CONNECTING = 101 /**< We're still connecting. This state is only entered when AVAHI_CLIENT_NO_FAIL has been passed to avahi_client_new() and the daemon is not yet available. */ + }; + + enum EntryGroupState { + ENTRY_GROUP_UNCOMMITED, /**< The group has not yet been commited, the user must still call avahi_entry_group_commit() */ + ENTRY_GROUP_REGISTERING, /**< The entries of the group are currently being registered */ + ENTRY_GROUP_ESTABLISHED, /**< The entries have successfully been established */ + ENTRY_GROUP_COLLISION, /**< A name collision for one of the entries in the group has been detected, the entries have been withdrawn */ + ENTRY_GROUP_FAILURE /**< Some kind of failure happened, the entries have been withdrawn */ + }; + + enum ClientFlags { + CLIENT_IGNORE_USER_CONFIG = 1, /**< Don't read user configuration */ + CLIENT_NO_FAIL = 2 /**< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear */ + }; + + /** Some flags for publishing functions */ + enum PublishFlags { + PUBLISH_UNIQUE = 1, /**< For raw records: The RRset is intended to be unique */ + PUBLISH_NO_PROBE = 2, /**< For raw records: Though the RRset is intended to be unique no probes shall be sent */ + PUBLISH_NO_ANNOUNCE = 4, /**< For raw records: Do not announce this RR to other hosts */ + PUBLISH_ALLOW_MULTIPLE = 8, /**< For raw records: Allow multiple local records of this type, even if they are intended to be unique */ + /** \cond fulldocs */ + PUBLISH_NO_REVERSE = 16, /**< For address records: don't create a reverse (PTR) entry */ + PUBLISH_NO_COOKIE = 32, /**< For service records: do not implicitly add the local service cookie to TXT data */ + /** \endcond */ + PUBLISH_UPDATE = 64, /**< Update existing records instead of adding new ones */ + /** \cond fulldocs */ + PUBLISH_USE_WIDE_AREA = 128, /**< Register the record using wide area DNS (i.e. unicast DNS update) */ + PUBLISH_USE_MULTICAST = 256 /**< Register the record using multicast DNS */ + /** \endcond */ + }; + + using IfIndex = int; + using Protocol = int; + + struct EntryGroup; + struct Poll; + struct SimplePoll; + struct Client; + + typedef void (*ClientCallback)(Client *, ClientState, void *userdata); + typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata); + + typedef void (*free_fn)(void *); + + typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error); + typedef void (*client_free_fn)(Client *); + typedef char *(*alternative_service_name_fn)(char *); + + typedef Client *(*entry_group_get_client_fn)(EntryGroup *); + + typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata); + typedef int (*entry_group_add_service_fn)( + EntryGroup *group, + IfIndex interface, + Protocol protocol, + PublishFlags flags, + const char *name, + const char *type, + const char *domain, + const char *host, + uint16_t port, + ...); + + typedef int (*entry_group_is_empty_fn)(EntryGroup *); + typedef int (*entry_group_reset_fn)(EntryGroup *); + typedef int (*entry_group_commit_fn)(EntryGroup *); + + typedef char *(*strdup_fn)(const char *); + typedef char *(*strerror_fn)(int); + typedef int (*client_errno_fn)(Client *); + + typedef Poll *(*simple_poll_get_fn)(SimplePoll *); + typedef int (*simple_poll_loop_fn)(SimplePoll *); + typedef void (*simple_poll_quit_fn)(SimplePoll *); + typedef SimplePoll *(*simple_poll_new_fn)(); + typedef void (*simple_poll_free_fn)(SimplePoll *); + + free_fn free; + client_new_fn client_new; + client_free_fn client_free; + alternative_service_name_fn alternative_service_name; + entry_group_get_client_fn entry_group_get_client; + entry_group_new_fn entry_group_new; + entry_group_add_service_fn entry_group_add_service; + entry_group_is_empty_fn entry_group_is_empty; + entry_group_reset_fn entry_group_reset; + entry_group_commit_fn entry_group_commit; + strdup_fn strdup; + strerror_fn strerror; + client_errno_fn client_errno; + simple_poll_get_fn simple_poll_get; + simple_poll_loop_fn simple_poll_loop; + simple_poll_quit_fn simple_poll_quit; + simple_poll_new_fn simple_poll_new; + simple_poll_free_fn simple_poll_free; + + int + init_common() { + static void *handle { nullptr }; + static bool funcs_loaded = false; + + if (funcs_loaded) return 0; + + if (!handle) { + handle = dyn::handle({ "libavahi-common.3.dylib", "libavahi-common.dylib" }); + if (!handle) { + return -1; + } + } + + std::vector> funcs { + { (dyn::apiproc *) &alternative_service_name, "avahi_alternative_service_name" }, + { (dyn::apiproc *) &free, "avahi_free" }, + { (dyn::apiproc *) &strdup, "avahi_strdup" }, + { (dyn::apiproc *) &strerror, "avahi_strerror" }, + { (dyn::apiproc *) &simple_poll_get, "avahi_simple_poll_get" }, + { (dyn::apiproc *) &simple_poll_loop, "avahi_simple_poll_loop" }, + { (dyn::apiproc *) &simple_poll_quit, "avahi_simple_poll_quit" }, + { (dyn::apiproc *) &simple_poll_new, "avahi_simple_poll_new" }, + { (dyn::apiproc *) &simple_poll_free, "avahi_simple_poll_free" }, + }; + + if (dyn::load(handle, funcs)) { + return -1; + } + + funcs_loaded = true; + return 0; } - funcs_loaded = true; - return 0; -} -} // namespace avahi + int + init_client() { + if (init_common()) { + return -1; + } + + static void *handle { nullptr }; + static bool funcs_loaded = false; + + if (funcs_loaded) return 0; + + if (!handle) { + handle = dyn::handle({ "libavahi-client.3.dylib", "libavahi-client.dylib" }); + if (!handle) { + return -1; + } + } + + std::vector> funcs { + { (dyn::apiproc *) &client_new, "avahi_client_new" }, + { (dyn::apiproc *) &client_free, "avahi_client_free" }, + { (dyn::apiproc *) &entry_group_get_client, "avahi_entry_group_get_client" }, + { (dyn::apiproc *) &entry_group_new, "avahi_entry_group_new" }, + { (dyn::apiproc *) &entry_group_add_service, "avahi_entry_group_add_service" }, + { (dyn::apiproc *) &entry_group_is_empty, "avahi_entry_group_is_empty" }, + { (dyn::apiproc *) &entry_group_reset, "avahi_entry_group_reset" }, + { (dyn::apiproc *) &entry_group_commit, "avahi_entry_group_commit" }, + { (dyn::apiproc *) &client_errno, "avahi_client_errno" }, + }; + + if (dyn::load(handle, funcs)) { + return -1; + } + + funcs_loaded = true; + return 0; + } +} // namespace avahi namespace platf::publish { -template -void free(T *p) { - avahi::free(p); -} - -template -using ptr_t = util::safe_ptr>; -using client_t = util::dyn_safe_ptr; -using poll_t = util::dyn_safe_ptr; - -avahi::EntryGroup *group = nullptr; - -poll_t poll; -client_t client; - -ptr_t name; - -void create_services(avahi::Client *c); - -void entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) { - group = g; - - switch(state) { - case avahi::ENTRY_GROUP_ESTABLISHED: - BOOST_LOG(info) << "Avahi service " << name.get() << " successfully established."; - break; - case avahi::ENTRY_GROUP_COLLISION: - name.reset(avahi::alternative_service_name(name.get())); - - BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get(); - - create_services(avahi::entry_group_get_client(g)); - break; - case avahi::ENTRY_GROUP_FAILURE: - BOOST_LOG(error) << "Avahi entry group failure: " << avahi::strerror(avahi::client_errno(avahi::entry_group_get_client(g))); - avahi::simple_poll_quit(poll.get()); - break; - case avahi::ENTRY_GROUP_UNCOMMITED: - case avahi::ENTRY_GROUP_REGISTERING:; + template + void + free(T *p) { + avahi::free(p); } -} -void create_services(avahi::Client *c) { - int ret; + template + using ptr_t = util::safe_ptr>; + using client_t = util::dyn_safe_ptr; + using poll_t = util::dyn_safe_ptr; - auto fg = util::fail_guard([]() { - avahi::simple_poll_quit(poll.get()); - }); + avahi::EntryGroup *group = nullptr; - if(!group) { - if(!(group = avahi::entry_group_new(c, entry_group_callback, nullptr))) { - BOOST_LOG(error) << "avahi::entry_group_new() failed: "sv << avahi::strerror(avahi::client_errno(c)); - return; + poll_t poll; + client_t client; + + ptr_t name; + + void + create_services(avahi::Client *c); + + void + entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) { + group = g; + + switch (state) { + case avahi::ENTRY_GROUP_ESTABLISHED: + BOOST_LOG(info) << "Avahi service " << name.get() << " successfully established."; + break; + case avahi::ENTRY_GROUP_COLLISION: + name.reset(avahi::alternative_service_name(name.get())); + + BOOST_LOG(info) << "Avahi service name collision, renaming service to " << name.get(); + + create_services(avahi::entry_group_get_client(g)); + break; + case avahi::ENTRY_GROUP_FAILURE: + BOOST_LOG(error) << "Avahi entry group failure: " << avahi::strerror(avahi::client_errno(avahi::entry_group_get_client(g))); + avahi::simple_poll_quit(poll.get()); + break; + case avahi::ENTRY_GROUP_UNCOMMITED: + case avahi::ENTRY_GROUP_REGISTERING:; } } - if(avahi::entry_group_is_empty(group)) { - BOOST_LOG(info) << "Adding avahi service "sv << name.get(); + void + create_services(avahi::Client *c) { + int ret; - ret = avahi::entry_group_add_service( - group, - avahi::IF_UNSPEC, avahi::PROTO_UNSPEC, - avahi::PublishFlags(0), - name.get(), - SERVICE_TYPE, - nullptr, nullptr, - map_port(nvhttp::PORT_HTTP), - nullptr); + auto fg = util::fail_guard([]() { + avahi::simple_poll_quit(poll.get()); + }); - if(ret < 0) { - if(ret == avahi::ERR_COLLISION) { - // A service name collision with a local service happened. Let's pick a new name - name.reset(avahi::alternative_service_name(name.get())); - BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get(); + if (!group) { + if (!(group = avahi::entry_group_new(c, entry_group_callback, nullptr))) { + BOOST_LOG(error) << "avahi::entry_group_new() failed: "sv << avahi::strerror(avahi::client_errno(c)); + return; + } + } - avahi::entry_group_reset(group); + if (avahi::entry_group_is_empty(group)) { + BOOST_LOG(info) << "Adding avahi service "sv << name.get(); - create_services(c); + ret = avahi::entry_group_add_service( + group, + avahi::IF_UNSPEC, avahi::PROTO_UNSPEC, + avahi::PublishFlags(0), + name.get(), + SERVICE_TYPE, + nullptr, nullptr, + map_port(nvhttp::PORT_HTTP), + nullptr); - fg.disable(); + if (ret < 0) { + if (ret == avahi::ERR_COLLISION) { + // A service name collision with a local service happened. Let's pick a new name + name.reset(avahi::alternative_service_name(name.get())); + BOOST_LOG(info) << "Service name collision, renaming service to "sv << name.get(); + + avahi::entry_group_reset(group); + + create_services(c); + + fg.disable(); + return; + } + + BOOST_LOG(error) << "Failed to add "sv << SERVICE_TYPE << " service: "sv << avahi::strerror(ret); return; } - BOOST_LOG(error) << "Failed to add "sv << SERVICE_TYPE << " service: "sv << avahi::strerror(ret); - return; + ret = avahi::entry_group_commit(group); + if (ret < 0) { + BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret); + return; + } } - ret = avahi::entry_group_commit(group); - if(ret < 0) { - BOOST_LOG(error) << "Failed to commit entry group: "sv << avahi::strerror(ret); - return; + fg.disable(); + } + + void + client_callback(avahi::Client *c, avahi::ClientState state, void *) { + switch (state) { + case avahi::CLIENT_S_RUNNING: + create_services(c); + break; + case avahi::CLIENT_FAILURE: + BOOST_LOG(error) << "Client failure: "sv << avahi::strerror(avahi::client_errno(c)); + avahi::simple_poll_quit(poll.get()); + break; + case avahi::CLIENT_S_COLLISION: + case avahi::CLIENT_S_REGISTERING: + if (group) + avahi::entry_group_reset(group); + break; + case avahi::CLIENT_CONNECTING:; } } - fg.disable(); -} + class deinit_t: public ::platf::deinit_t { + public: + std::thread poll_thread; -void client_callback(avahi::Client *c, avahi::ClientState state, void *) { - switch(state) { - case avahi::CLIENT_S_RUNNING: - create_services(c); - break; - case avahi::CLIENT_FAILURE: - BOOST_LOG(error) << "Client failure: "sv << avahi::strerror(avahi::client_errno(c)); - avahi::simple_poll_quit(poll.get()); - break; - case avahi::CLIENT_S_COLLISION: - case avahi::CLIENT_S_REGISTERING: - if(group) - avahi::entry_group_reset(group); - break; - case avahi::CLIENT_CONNECTING:; - } -} + deinit_t(std::thread poll_thread): + poll_thread { std::move(poll_thread) } {} -class deinit_t : public ::platf::deinit_t { -public: - std::thread poll_thread; + ~deinit_t() override { + if (avahi::simple_poll_quit && poll) { + avahi::simple_poll_quit(poll.get()); + } - deinit_t(std::thread poll_thread) : poll_thread { std::move(poll_thread) } {} + if (poll_thread.joinable()) { + poll_thread.join(); + } + } + }; - ~deinit_t() override { - if(avahi::simple_poll_quit && poll) { - avahi::simple_poll_quit(poll.get()); + [[nodiscard]] std::unique_ptr<::platf::deinit_t> + start() { + if (avahi::init_client()) { + return nullptr; } - if(poll_thread.joinable()) { - poll_thread.join(); + int avhi_error; + + poll.reset(avahi::simple_poll_new()); + if (!poll) { + BOOST_LOG(error) << "Failed to create simple poll object."sv; + return nullptr; } + + name.reset(avahi::strdup(SERVICE_NAME)); + + client.reset( + avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error)); + + if (!client) { + BOOST_LOG(error) << "Failed to create client: "sv << avahi::strerror(avhi_error); + return nullptr; + } + + return std::make_unique(std::thread { avahi::simple_poll_loop, poll.get() }); } -}; - -[[nodiscard]] std::unique_ptr<::platf::deinit_t> start() { - if(avahi::init_client()) { - return nullptr; - } - - int avhi_error; - - poll.reset(avahi::simple_poll_new()); - if(!poll) { - BOOST_LOG(error) << "Failed to create simple poll object."sv; - return nullptr; - } - - name.reset(avahi::strdup(SERVICE_NAME)); - - client.reset( - avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error)); - - if(!client) { - BOOST_LOG(error) << "Failed to create client: "sv << avahi::strerror(avhi_error); - return nullptr; - } - - return std::make_unique(std::thread { avahi::simple_poll_loop, poll.get() }); -} -}; // namespace platf::publish +}; // namespace platf::publish diff --git a/src/platform/windows/PolicyConfig.h b/src/platform/windows/PolicyConfig.h index be18ec6f..b0a1264e 100644 --- a/src/platform/windows/PolicyConfig.h +++ b/src/platform/windows/PolicyConfig.h @@ -6,16 +6,15 @@ // http://eretik.omegahg.com/ // ---------------------------------------------------------------------------- - #pragma once #ifdef __MINGW32__ -#undef DEFINE_GUID -#ifdef __cplusplus -#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) EXTERN_C const GUID DECLSPEC_SELECTANY name = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } } -#else -#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) const GUID DECLSPEC_SELECTANY name = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } } -#endif + #undef DEFINE_GUID + #ifdef __cplusplus + #define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) EXTERN_C const GUID DECLSPEC_SELECTANY name = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } } + #else + #define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) const GUID DECLSPEC_SELECTANY name = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } } + #endif DEFINE_GUID(IID_IPolicyConfig, 0xf8679f50, 0x850a, 0x41cf, 0x9c, 0x72, 0x43, 0x0f, 0x29, 0x02, 0x90, 0xc8); DEFINE_GUID(CLSID_CPolicyConfigClient, 0x870af99c, 0x171d, 0x4f9e, 0xaf, 0x0d, 0xe6, 0x3d, 0xf4, 0x0c, 0x2b, 0xc9); @@ -37,13 +36,15 @@ class DECLSPEC_UUID("870af99c-171d-4f9e-af0d-e63df40c2bc9") CPolicyConfigClient; // // @compatible: Windows 7 and Later // ---------------------------------------------------------------------------- -interface IPolicyConfig : public IUnknown { +interface IPolicyConfig: public IUnknown { public: - virtual HRESULT GetMixFormat( + virtual HRESULT + GetMixFormat( PCWSTR, WAVEFORMATEX **); - virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat( + virtual HRESULT STDMETHODCALLTYPE + GetDeviceFormat( PCWSTR, INT, WAVEFORMATEX **); @@ -51,7 +52,8 @@ public: virtual HRESULT STDMETHODCALLTYPE ResetDeviceFormat( PCWSTR); - virtual HRESULT STDMETHODCALLTYPE SetDeviceFormat( + virtual HRESULT STDMETHODCALLTYPE + SetDeviceFormat( PCWSTR, WAVEFORMATEX *, WAVEFORMATEX *); @@ -66,25 +68,30 @@ public: PCWSTR, PINT64); - virtual HRESULT STDMETHODCALLTYPE GetShareMode( + virtual HRESULT STDMETHODCALLTYPE + GetShareMode( PCWSTR, struct DeviceShareMode *); - virtual HRESULT STDMETHODCALLTYPE SetShareMode( + virtual HRESULT STDMETHODCALLTYPE + SetShareMode( PCWSTR, struct DeviceShareMode *); - virtual HRESULT STDMETHODCALLTYPE GetPropertyValue( + virtual HRESULT STDMETHODCALLTYPE + GetPropertyValue( PCWSTR, const PROPERTYKEY &, PROPVARIANT *); - virtual HRESULT STDMETHODCALLTYPE SetPropertyValue( + virtual HRESULT STDMETHODCALLTYPE + SetPropertyValue( PCWSTR, const PROPERTYKEY &, PROPVARIANT *); - virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint( + virtual HRESULT STDMETHODCALLTYPE + SetDefaultEndpoint( PCWSTR wszDeviceId, ERole eRole); @@ -108,18 +115,21 @@ class DECLSPEC_UUID("294935CE-F637-4E7C-A41B-AB255460B862") CPolicyConfigVistaCl // // @compatible: Windows Vista and Later // ---------------------------------------------------------------------------- -interface IPolicyConfigVista : public IUnknown { +interface IPolicyConfigVista: public IUnknown { public: - virtual HRESULT GetMixFormat( + virtual HRESULT + GetMixFormat( PCWSTR, - WAVEFORMATEX **); // not available on Windows 7, use method from IPolicyConfig + WAVEFORMATEX **); // not available on Windows 7, use method from IPolicyConfig - virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat( + virtual HRESULT STDMETHODCALLTYPE + GetDeviceFormat( PCWSTR, INT, WAVEFORMATEX **); - virtual HRESULT STDMETHODCALLTYPE SetDeviceFormat( + virtual HRESULT STDMETHODCALLTYPE + SetDeviceFormat( PCWSTR, WAVEFORMATEX *, WAVEFORMATEX *); @@ -128,37 +138,42 @@ public: PCWSTR, INT, PINT64, - PINT64); // not available on Windows 7, use method from IPolicyConfig + PINT64); // not available on Windows 7, use method from IPolicyConfig virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod( PCWSTR, - PINT64); // not available on Windows 7, use method from IPolicyConfig + PINT64); // not available on Windows 7, use method from IPolicyConfig - virtual HRESULT STDMETHODCALLTYPE GetShareMode( + virtual HRESULT STDMETHODCALLTYPE + GetShareMode( PCWSTR, - struct DeviceShareMode *); // not available on Windows 7, use method from IPolicyConfig + struct DeviceShareMode *); // not available on Windows 7, use method from IPolicyConfig - virtual HRESULT STDMETHODCALLTYPE SetShareMode( + virtual HRESULT STDMETHODCALLTYPE + SetShareMode( PCWSTR, - struct DeviceShareMode *); // not available on Windows 7, use method from IPolicyConfig + struct DeviceShareMode *); // not available on Windows 7, use method from IPolicyConfig - virtual HRESULT STDMETHODCALLTYPE GetPropertyValue( + virtual HRESULT STDMETHODCALLTYPE + GetPropertyValue( PCWSTR, const PROPERTYKEY &, PROPVARIANT *); - virtual HRESULT STDMETHODCALLTYPE SetPropertyValue( + virtual HRESULT STDMETHODCALLTYPE + SetPropertyValue( PCWSTR, const PROPERTYKEY &, PROPVARIANT *); - virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint( + virtual HRESULT STDMETHODCALLTYPE + SetDefaultEndpoint( PCWSTR wszDeviceId, ERole eRole); virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility( PCWSTR, - INT); // not available on Windows 7, use method from IPolicyConfig + INT); // not available on Windows 7, use method from IPolicyConfig }; // ---------------------------------------------------------------------------- diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index 72360f53..9f38f5b6 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -23,84 +23,112 @@ #include "PolicyConfig.h" // clang-format on -DEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2); // DEVPROP_TYPE_STRING -DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14); // DEVPROP_TYPE_STRING DEFINE_PROPERTYKEY(PKEY_DeviceInterface_FriendlyName, 0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22, 2); const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator); -const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); -const IID IID_IAudioClient = __uuidof(IAudioClient); -const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient); +const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); +const IID IID_IAudioClient = __uuidof(IAudioClient); +const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient); using namespace std::literals; namespace platf::audio { -constexpr auto SAMPLE_RATE = 48000; + constexpr auto SAMPLE_RATE = 48000; -template -void Release(T *p) { - p->Release(); -} - -template -void co_task_free(T *p) { - CoTaskMemFree((LPVOID)p); -} - -using device_enum_t = util::safe_ptr>; -using device_t = util::safe_ptr>; -using collection_t = util::safe_ptr>; -using audio_client_t = util::safe_ptr>; -using audio_capture_t = util::safe_ptr>; -using wave_format_t = util::safe_ptr>; -using wstring_t = util::safe_ptr>; -using handle_t = util::safe_ptr_v2; -using policy_t = util::safe_ptr>; -using prop_t = util::safe_ptr>; - -class co_init_t : public deinit_t { -public: - co_init_t() { - CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY); + template + void + Release(T *p) { + p->Release(); } - ~co_init_t() override { - CoUninitialize(); - } -}; - -class prop_var_t { -public: - prop_var_t() { - PropVariantInit(&prop); + template + void + co_task_free(T *p) { + CoTaskMemFree((LPVOID) p); } - ~prop_var_t() { - PropVariantClear(&prop); - } + using device_enum_t = util::safe_ptr>; + using device_t = util::safe_ptr>; + using collection_t = util::safe_ptr>; + using audio_client_t = util::safe_ptr>; + using audio_capture_t = util::safe_ptr>; + using wave_format_t = util::safe_ptr>; + using wstring_t = util::safe_ptr>; + using handle_t = util::safe_ptr_v2; + using policy_t = util::safe_ptr>; + using prop_t = util::safe_ptr>; - PROPVARIANT prop; -}; + class co_init_t: public deinit_t { + public: + co_init_t() { + CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY); + } -static std::wstring_convert, wchar_t> converter; -struct format_t { - enum type_e : int { - none, - stereo, - surr51, - surr71, - } type; + ~co_init_t() override { + CoUninitialize(); + } + }; - std::string_view name; - int channels; - int channel_mask; -} formats[] { - { - format_t::stereo, - "Stereo"sv, - 2, - SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT, - }, - { + class prop_var_t { + public: + prop_var_t() { + PropVariantInit(&prop); + } + + ~prop_var_t() { + PropVariantClear(&prop); + } + + PROPVARIANT prop; + }; + + static std::wstring_convert, wchar_t> converter; + struct format_t { + enum type_e : int { + none, + stereo, + surr51, + surr71, + } type; + + std::string_view name; + int channels; + int channel_mask; + } formats[] { + { + format_t::stereo, + "Stereo"sv, + 2, + SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT, + }, + { + format_t::surr51, + "Surround 5.1"sv, + 6, + SPEAKER_FRONT_LEFT | + SPEAKER_FRONT_RIGHT | + SPEAKER_FRONT_CENTER | + SPEAKER_LOW_FREQUENCY | + SPEAKER_BACK_LEFT | + SPEAKER_BACK_RIGHT, + }, + { + format_t::surr71, + "Surround 7.1"sv, + 8, + SPEAKER_FRONT_LEFT | + SPEAKER_FRONT_RIGHT | + SPEAKER_FRONT_CENTER | + SPEAKER_LOW_FREQUENCY | + SPEAKER_BACK_LEFT | + SPEAKER_BACK_RIGHT | + SPEAKER_SIDE_LEFT | + SPEAKER_SIDE_RIGHT, + }, + }; + + static format_t surround_51_side_speakers { format_t::surr51, "Surround 5.1"sv, 6, @@ -108,462 +136,445 @@ struct format_t { SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | - SPEAKER_BACK_LEFT | - SPEAKER_BACK_RIGHT, - }, - { - format_t::surr71, - "Surround 7.1"sv, - 8, - SPEAKER_FRONT_LEFT | - SPEAKER_FRONT_RIGHT | - SPEAKER_FRONT_CENTER | - SPEAKER_LOW_FREQUENCY | - SPEAKER_BACK_LEFT | - SPEAKER_BACK_RIGHT | SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT, - }, -}; - -static format_t surround_51_side_speakers { - format_t::surr51, - "Surround 5.1"sv, - 6, - SPEAKER_FRONT_LEFT | - SPEAKER_FRONT_RIGHT | - SPEAKER_FRONT_CENTER | - SPEAKER_LOW_FREQUENCY | - SPEAKER_SIDE_LEFT | - SPEAKER_SIDE_RIGHT, -}; - -WAVEFORMATEXTENSIBLE create_wave_format(const format_t &format) { - WAVEFORMATEXTENSIBLE wave_format; - - wave_format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; - wave_format.Format.nChannels = format.channels; - wave_format.Format.nSamplesPerSec = SAMPLE_RATE; - wave_format.Format.wBitsPerSample = 16; - wave_format.Format.nBlockAlign = wave_format.Format.nChannels * wave_format.Format.wBitsPerSample / 8; - wave_format.Format.nAvgBytesPerSec = wave_format.Format.nSamplesPerSec * wave_format.Format.nBlockAlign; - wave_format.Format.cbSize = sizeof(wave_format); - - wave_format.Samples.wValidBitsPerSample = 16; - wave_format.dwChannelMask = format.channel_mask; - wave_format.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; - - return wave_format; -} - -int set_wave_format(audio::wave_format_t &wave_format, const format_t &format) { - wave_format->nSamplesPerSec = SAMPLE_RATE; - wave_format->wBitsPerSample = 16; - - switch(wave_format->wFormatTag) { - case WAVE_FORMAT_PCM: - break; - case WAVE_FORMAT_IEEE_FLOAT: - break; - case WAVE_FORMAT_EXTENSIBLE: { - auto wave_ex = (PWAVEFORMATEXTENSIBLE)wave_format.get(); - wave_ex->Samples.wValidBitsPerSample = 16; - wave_ex->dwChannelMask = format.channel_mask; - wave_ex->SubFormat = KSDATAFORMAT_SUBTYPE_PCM; - break; - } - default: - BOOST_LOG(error) << "Unsupported Wave Format: [0x"sv << util::hex(wave_format->wFormatTag).to_string_view() << ']'; - return -1; }; - wave_format->nChannels = format.channels; - wave_format->nBlockAlign = wave_format->nChannels * wave_format->wBitsPerSample / 8; - wave_format->nAvgBytesPerSec = wave_format->nSamplesPerSec * wave_format->nBlockAlign; + WAVEFORMATEXTENSIBLE + create_wave_format(const format_t &format) { + WAVEFORMATEXTENSIBLE wave_format; - return 0; -} + wave_format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + wave_format.Format.nChannels = format.channels; + wave_format.Format.nSamplesPerSec = SAMPLE_RATE; + wave_format.Format.wBitsPerSample = 16; + wave_format.Format.nBlockAlign = wave_format.Format.nChannels * wave_format.Format.wBitsPerSample / 8; + wave_format.Format.nAvgBytesPerSec = wave_format.Format.nSamplesPerSec * wave_format.Format.nBlockAlign; + wave_format.Format.cbSize = sizeof(wave_format); -audio_client_t make_audio_client(device_t &device, const format_t &format) { - audio_client_t audio_client; - auto status = device->Activate( - IID_IAudioClient, - CLSCTX_ALL, - nullptr, - (void **)&audio_client); + wave_format.Samples.wValidBitsPerSample = 16; + wave_format.dwChannelMask = format.channel_mask; + wave_format.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; - if(FAILED(status)) { - BOOST_LOG(error) << "Couldn't activate Device: [0x"sv << util::hex(status).to_string_view() << ']'; - - return nullptr; + return wave_format; } - WAVEFORMATEXTENSIBLE wave_format = create_wave_format(format); + int + set_wave_format(audio::wave_format_t &wave_format, const format_t &format) { + wave_format->nSamplesPerSec = SAMPLE_RATE; + wave_format->wBitsPerSample = 16; - status = audio_client->Initialize( - AUDCLNT_SHAREMODE_SHARED, - AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK | - AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, // Enable automatic resampling to 48 KHz - 0, 0, - (LPWAVEFORMATEX)&wave_format, - nullptr); - - if(status) { - BOOST_LOG(debug) << "Couldn't initialize audio client for ["sv << format.name << "]: [0x"sv << util::hex(status).to_string_view() << ']'; - return nullptr; - } - - return audio_client; -} - -const wchar_t *no_null(const wchar_t *str) { - return str ? str : L"Unknown"; -} - -bool validate_device(device_t &device) { - bool valid = false; - - // Check for any valid format - for(const auto &format : formats) { - auto audio_client = make_audio_client(device, format); - - BOOST_LOG(debug) << format.name << ": "sv << (!audio_client ? "unsupported"sv : "supported"sv); - - if(audio_client) { - valid = true; - } - } - - return valid; -} - -device_t default_device(device_enum_t &device_enum) { - device_t device; - HRESULT status; - status = device_enum->GetDefaultAudioEndpoint( - eRender, - eConsole, - &device); - - - if(FAILED(status)) { - BOOST_LOG(error) << "Couldn't create audio Device [0x"sv << util::hex(status).to_string_view() << ']'; - - return nullptr; - } - - return device; -} - -class mic_wasapi_t : public mic_t { -public: - capture_e sample(std::vector &sample_out) override { - auto sample_size = sample_out.size(); - - // Refill the sample buffer if needed - while(sample_buf_pos - std::begin(sample_buf) < sample_size) { - auto capture_result = _fill_buffer(); - if(capture_result != capture_e::ok) { - return capture_result; - } - } - - // Fill the output buffer with samples - std::copy_n(std::begin(sample_buf), sample_size, std::begin(sample_out)); - - // Move any excess samples to the front of the buffer - std::move(&sample_buf[sample_size], sample_buf_pos, std::begin(sample_buf)); - sample_buf_pos -= sample_size; - - return capture_e::ok; - } - - - int init(std::uint32_t sample_rate, std::uint32_t frame_size, std::uint32_t channels_out) { - audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr)); - if(!audio_event) { - BOOST_LOG(error) << "Couldn't create Event handle"sv; - - return -1; - } - - HRESULT status; - - status = CoCreateInstance( - CLSID_MMDeviceEnumerator, - nullptr, - CLSCTX_ALL, - IID_IMMDeviceEnumerator, - (void **)&device_enum); - - if(FAILED(status)) { - BOOST_LOG(error) << "Couldn't create Device Enumerator [0x"sv << util::hex(status).to_string_view() << ']'; - - return -1; - } - - auto device = default_device(device_enum); - if(!device) { - return -1; - } - - for(auto &format : formats) { - if(format.channels != channels_out) { - BOOST_LOG(debug) << "Skipping audio format ["sv << format.name << "] with channel count ["sv << format.channels << " != "sv << channels_out << ']'; - continue; - } - - BOOST_LOG(debug) << "Trying audio format ["sv << format.name << ']'; - audio_client = make_audio_client(device, format); - - if(audio_client) { - BOOST_LOG(debug) << "Found audio format ["sv << format.name << ']'; - channels = channels_out; + switch (wave_format->wFormatTag) { + case WAVE_FORMAT_PCM: + break; + case WAVE_FORMAT_IEEE_FLOAT: + break; + case WAVE_FORMAT_EXTENSIBLE: { + auto wave_ex = (PWAVEFORMATEXTENSIBLE) wave_format.get(); + wave_ex->Samples.wValidBitsPerSample = 16; + wave_ex->dwChannelMask = format.channel_mask; + wave_ex->SubFormat = KSDATAFORMAT_SUBTYPE_PCM; break; } - } + default: + BOOST_LOG(error) << "Unsupported Wave Format: [0x"sv << util::hex(wave_format->wFormatTag).to_string_view() << ']'; + return -1; + }; - if(!audio_client) { - BOOST_LOG(error) << "Couldn't find supported format for audio"sv; - return -1; - } - - REFERENCE_TIME default_latency; - audio_client->GetDevicePeriod(&default_latency, nullptr); - default_latency_ms = default_latency / 1000; - - std::uint32_t frames; - status = audio_client->GetBufferSize(&frames); - if(FAILED(status)) { - BOOST_LOG(error) << "Couldn't acquire the number of audio frames [0x"sv << util::hex(status).to_string_view() << ']'; - - return -1; - } - - // *2 --> needs to fit double - sample_buf = util::buffer_t { std::max(frames, frame_size) * 2 * channels_out }; - sample_buf_pos = std::begin(sample_buf); - - status = audio_client->GetService(IID_IAudioCaptureClient, (void **)&audio_capture); - if(FAILED(status)) { - BOOST_LOG(error) << "Couldn't initialize audio capture client [0x"sv << util::hex(status).to_string_view() << ']'; - - return -1; - } - - status = audio_client->SetEventHandle(audio_event.get()); - if(FAILED(status)) { - BOOST_LOG(error) << "Couldn't set event handle [0x"sv << util::hex(status).to_string_view() << ']'; - - return -1; - } - - status = audio_client->Start(); - if(FAILED(status)) { - BOOST_LOG(error) << "Couldn't start recording [0x"sv << util::hex(status).to_string_view() << ']'; - - return -1; - } + wave_format->nChannels = format.channels; + wave_format->nBlockAlign = wave_format->nChannels * wave_format->wBitsPerSample / 8; + wave_format->nAvgBytesPerSec = wave_format->nSamplesPerSec * wave_format->nBlockAlign; return 0; } - ~mic_wasapi_t() override { - if(audio_client) { - audio_client->Stop(); + audio_client_t + make_audio_client(device_t &device, const format_t &format) { + audio_client_t audio_client; + auto status = device->Activate( + IID_IAudioClient, + CLSCTX_ALL, + nullptr, + (void **) &audio_client); + + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't activate Device: [0x"sv << util::hex(status).to_string_view() << ']'; + + return nullptr; } + + WAVEFORMATEXTENSIBLE wave_format = create_wave_format(format); + + status = audio_client->Initialize( + AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK | + AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, // Enable automatic resampling to 48 KHz + 0, 0, + (LPWAVEFORMATEX) &wave_format, + nullptr); + + if (status) { + BOOST_LOG(debug) << "Couldn't initialize audio client for ["sv << format.name << "]: [0x"sv << util::hex(status).to_string_view() << ']'; + return nullptr; + } + + return audio_client; } -private: - capture_e _fill_buffer() { - HRESULT status; + const wchar_t * + no_null(const wchar_t *str) { + return str ? str : L"Unknown"; + } - // Total number of samples - struct sample_aligned_t { - std::uint32_t uninitialized; - std::int16_t *samples; - } sample_aligned; + bool + validate_device(device_t &device) { + bool valid = false; - // number of samples / number of channels - struct block_aligned_t { - std::uint32_t audio_sample_size; - } block_aligned; + // Check for any valid format + for (const auto &format : formats) { + auto audio_client = make_audio_client(device, format); - status = WaitForSingleObjectEx(audio_event.get(), default_latency_ms, FALSE); - switch(status) { - case WAIT_OBJECT_0: - break; - case WAIT_TIMEOUT: - return capture_e::timeout; - default: - BOOST_LOG(error) << "Couldn't wait for audio event: [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; + BOOST_LOG(debug) << format.name << ": "sv << (!audio_client ? "unsupported"sv : "supported"sv); + + if (audio_client) { + valid = true; + } } - std::uint32_t packet_size {}; - for( - status = audio_capture->GetNextPacketSize(&packet_size); - SUCCEEDED(status) && packet_size > 0; - status = audio_capture->GetNextPacketSize(&packet_size)) { - DWORD buffer_flags; - status = audio_capture->GetBuffer( - (BYTE **)&sample_aligned.samples, - &block_aligned.audio_sample_size, - &buffer_flags, - nullptr, nullptr); + return valid; + } - switch(status) { - case S_OK: - break; - case AUDCLNT_E_DEVICE_INVALIDATED: + device_t + default_device(device_enum_t &device_enum) { + device_t device; + HRESULT status; + status = device_enum->GetDefaultAudioEndpoint( + eRender, + eConsole, + &device); + + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't create audio Device [0x"sv << util::hex(status).to_string_view() << ']'; + + return nullptr; + } + + return device; + } + + class mic_wasapi_t: public mic_t { + public: + capture_e + sample(std::vector &sample_out) override { + auto sample_size = sample_out.size(); + + // Refill the sample buffer if needed + while (sample_buf_pos - std::begin(sample_buf) < sample_size) { + auto capture_result = _fill_buffer(); + if (capture_result != capture_e::ok) { + return capture_result; + } + } + + // Fill the output buffer with samples + std::copy_n(std::begin(sample_buf), sample_size, std::begin(sample_out)); + + // Move any excess samples to the front of the buffer + std::move(&sample_buf[sample_size], sample_buf_pos, std::begin(sample_buf)); + sample_buf_pos -= sample_size; + + return capture_e::ok; + } + + int + init(std::uint32_t sample_rate, std::uint32_t frame_size, std::uint32_t channels_out) { + audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr)); + if (!audio_event) { + BOOST_LOG(error) << "Couldn't create Event handle"sv; + + return -1; + } + + HRESULT status; + + status = CoCreateInstance( + CLSID_MMDeviceEnumerator, + nullptr, + CLSCTX_ALL, + IID_IMMDeviceEnumerator, + (void **) &device_enum); + + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't create Device Enumerator [0x"sv << util::hex(status).to_string_view() << ']'; + + return -1; + } + + auto device = default_device(device_enum); + if (!device) { + return -1; + } + + for (auto &format : formats) { + if (format.channels != channels_out) { + BOOST_LOG(debug) << "Skipping audio format ["sv << format.name << "] with channel count ["sv << format.channels << " != "sv << channels_out << ']'; + continue; + } + + BOOST_LOG(debug) << "Trying audio format ["sv << format.name << ']'; + audio_client = make_audio_client(device, format); + + if (audio_client) { + BOOST_LOG(debug) << "Found audio format ["sv << format.name << ']'; + channels = channels_out; + break; + } + } + + if (!audio_client) { + BOOST_LOG(error) << "Couldn't find supported format for audio"sv; + return -1; + } + + REFERENCE_TIME default_latency; + audio_client->GetDevicePeriod(&default_latency, nullptr); + default_latency_ms = default_latency / 1000; + + std::uint32_t frames; + status = audio_client->GetBufferSize(&frames); + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't acquire the number of audio frames [0x"sv << util::hex(status).to_string_view() << ']'; + + return -1; + } + + // *2 --> needs to fit double + sample_buf = util::buffer_t { std::max(frames, frame_size) * 2 * channels_out }; + sample_buf_pos = std::begin(sample_buf); + + status = audio_client->GetService(IID_IAudioCaptureClient, (void **) &audio_capture); + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't initialize audio capture client [0x"sv << util::hex(status).to_string_view() << ']'; + + return -1; + } + + status = audio_client->SetEventHandle(audio_event.get()); + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't set event handle [0x"sv << util::hex(status).to_string_view() << ']'; + + return -1; + } + + status = audio_client->Start(); + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't start recording [0x"sv << util::hex(status).to_string_view() << ']'; + + return -1; + } + + return 0; + } + + ~mic_wasapi_t() override { + if (audio_client) { + audio_client->Stop(); + } + } + + private: + capture_e + _fill_buffer() { + HRESULT status; + + // Total number of samples + struct sample_aligned_t { + std::uint32_t uninitialized; + std::int16_t *samples; + } sample_aligned; + + // number of samples / number of channels + struct block_aligned_t { + std::uint32_t audio_sample_size; + } block_aligned; + + status = WaitForSingleObjectEx(audio_event.get(), default_latency_ms, FALSE); + switch (status) { + case WAIT_OBJECT_0: + break; + case WAIT_TIMEOUT: + return capture_e::timeout; + default: + BOOST_LOG(error) << "Couldn't wait for audio event: [0x"sv << util::hex(status).to_string_view() << ']'; + return capture_e::error; + } + + std::uint32_t packet_size {}; + for ( + status = audio_capture->GetNextPacketSize(&packet_size); + SUCCEEDED(status) && packet_size > 0; + status = audio_capture->GetNextPacketSize(&packet_size)) { + DWORD buffer_flags; + status = audio_capture->GetBuffer( + (BYTE **) &sample_aligned.samples, + &block_aligned.audio_sample_size, + &buffer_flags, + nullptr, nullptr); + + switch (status) { + case S_OK: + break; + case AUDCLNT_E_DEVICE_INVALIDATED: + return capture_e::reinit; + default: + BOOST_LOG(error) << "Couldn't capture audio [0x"sv << util::hex(status).to_string_view() << ']'; + return capture_e::error; + } + + sample_aligned.uninitialized = std::end(sample_buf) - sample_buf_pos; + auto n = std::min(sample_aligned.uninitialized, block_aligned.audio_sample_size * channels); + + if (buffer_flags & AUDCLNT_BUFFERFLAGS_SILENT) { + std::fill_n(sample_buf_pos, n, 0); + } + else { + std::copy_n(sample_aligned.samples, n, sample_buf_pos); + } + + sample_buf_pos += n; + + audio_capture->ReleaseBuffer(block_aligned.audio_sample_size); + } + + if (status == AUDCLNT_E_DEVICE_INVALIDATED) { return capture_e::reinit; - default: - BOOST_LOG(error) << "Couldn't capture audio [0x"sv << util::hex(status).to_string_view() << ']'; + } + + if (FAILED(status)) { return capture_e::error; } - sample_aligned.uninitialized = std::end(sample_buf) - sample_buf_pos; - auto n = std::min(sample_aligned.uninitialized, block_aligned.audio_sample_size * channels); + return capture_e::ok; + } - if(buffer_flags & AUDCLNT_BUFFERFLAGS_SILENT) { - std::fill_n(sample_buf_pos, n, 0); - } - else { - std::copy_n(sample_aligned.samples, n, sample_buf_pos); + public: + handle_t audio_event; + + device_enum_t device_enum; + device_t device; + audio_client_t audio_client; + audio_capture_t audio_capture; + + REFERENCE_TIME default_latency_ms; + + util::buffer_t sample_buf; + std::int16_t *sample_buf_pos; + int channels; + }; + + class audio_control_t: public ::platf::audio_control_t { + public: + std::optional + sink_info() override { + auto virtual_adapter_name = L"Steam Streaming Speakers"sv; + + sink_t sink; + + audio::device_enum_t device_enum; + auto status = CoCreateInstance( + CLSID_MMDeviceEnumerator, + nullptr, + CLSCTX_ALL, + IID_IMMDeviceEnumerator, + (void **) &device_enum); + + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't create Device Enumerator: [0x"sv << util::hex(status).to_string_view() << ']'; + + return std::nullopt; } - sample_buf_pos += n; - - audio_capture->ReleaseBuffer(block_aligned.audio_sample_size); - } - - if(status == AUDCLNT_E_DEVICE_INVALIDATED) { - return capture_e::reinit; - } - - if(FAILED(status)) { - return capture_e::error; - } - - return capture_e::ok; - } - -public: - handle_t audio_event; - - device_enum_t device_enum; - device_t device; - audio_client_t audio_client; - audio_capture_t audio_capture; - - REFERENCE_TIME default_latency_ms; - - util::buffer_t sample_buf; - std::int16_t *sample_buf_pos; - int channels; -}; - -class audio_control_t : public ::platf::audio_control_t { -public: - std::optional sink_info() override { - auto virtual_adapter_name = L"Steam Streaming Speakers"sv; - - sink_t sink; - - audio::device_enum_t device_enum; - auto status = CoCreateInstance( - CLSID_MMDeviceEnumerator, - nullptr, - CLSCTX_ALL, - IID_IMMDeviceEnumerator, - (void **)&device_enum); - - if(FAILED(status)) { - BOOST_LOG(error) << "Couldn't create Device Enumerator: [0x"sv << util::hex(status).to_string_view() << ']'; - - return std::nullopt; - } - - auto device = default_device(device_enum); - if(!device) { - return std::nullopt; - } - - audio::wstring_t wstring; - device->GetId(&wstring); - - sink.host = converter.to_bytes(wstring.get()); - - collection_t collection; - status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection); - if(FAILED(status)) { - BOOST_LOG(error) << "Couldn't enumerate: [0x"sv << util::hex(status).to_string_view() << ']'; - - return std::nullopt; - } - - UINT count; - collection->GetCount(&count); - - std::string virtual_device_id = config::audio.virtual_sink; - for(auto x = 0; x < count; ++x) { - audio::device_t device; - collection->Item(x, &device); - - if(!validate_device(device)) { - continue; + auto device = default_device(device_enum); + if (!device) { + return std::nullopt; } audio::wstring_t wstring; device->GetId(&wstring); - audio::prop_t prop; - device->OpenPropertyStore(STGM_READ, &prop); + sink.host = converter.to_bytes(wstring.get()); - prop_var_t adapter_friendly_name; - prop_var_t device_friendly_name; - prop_var_t device_desc; + collection_t collection; + status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection); + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't enumerate: [0x"sv << util::hex(status).to_string_view() << ']'; - prop->GetValue(PKEY_Device_FriendlyName, &device_friendly_name.prop); - prop->GetValue(PKEY_DeviceInterface_FriendlyName, &adapter_friendly_name.prop); - prop->GetValue(PKEY_Device_DeviceDesc, &device_desc.prop); - - auto adapter_name = no_null((LPWSTR)adapter_friendly_name.prop.pszVal); - BOOST_LOG(verbose) - << L"===== Device ====="sv << std::endl - << L"Device ID : "sv << wstring.get() << std::endl - << L"Device name : "sv << no_null((LPWSTR)device_friendly_name.prop.pszVal) << std::endl - << L"Adapter name : "sv << adapter_name << std::endl - << L"Device description : "sv << no_null((LPWSTR)device_desc.prop.pszVal) << std::endl - << std::endl; - - if(virtual_device_id.empty() && adapter_name == virtual_adapter_name) { - virtual_device_id = converter.to_bytes(wstring.get()); + return std::nullopt; } + + UINT count; + collection->GetCount(&count); + + std::string virtual_device_id = config::audio.virtual_sink; + for (auto x = 0; x < count; ++x) { + audio::device_t device; + collection->Item(x, &device); + + if (!validate_device(device)) { + continue; + } + + audio::wstring_t wstring; + device->GetId(&wstring); + + audio::prop_t prop; + device->OpenPropertyStore(STGM_READ, &prop); + + prop_var_t adapter_friendly_name; + prop_var_t device_friendly_name; + prop_var_t device_desc; + + prop->GetValue(PKEY_Device_FriendlyName, &device_friendly_name.prop); + prop->GetValue(PKEY_DeviceInterface_FriendlyName, &adapter_friendly_name.prop); + prop->GetValue(PKEY_Device_DeviceDesc, &device_desc.prop); + + auto adapter_name = no_null((LPWSTR) adapter_friendly_name.prop.pszVal); + BOOST_LOG(verbose) + << L"===== Device ====="sv << std::endl + << L"Device ID : "sv << wstring.get() << std::endl + << L"Device name : "sv << no_null((LPWSTR) device_friendly_name.prop.pszVal) << std::endl + << L"Adapter name : "sv << adapter_name << std::endl + << L"Device description : "sv << no_null((LPWSTR) device_desc.prop.pszVal) << std::endl + << std::endl; + + if (virtual_device_id.empty() && adapter_name == virtual_adapter_name) { + virtual_device_id = converter.to_bytes(wstring.get()); + } + } + + if (!virtual_device_id.empty()) { + sink.null = std::make_optional(sink_t::null_t { + "virtual-"s.append(formats[format_t::stereo - 1].name) + virtual_device_id, + "virtual-"s.append(formats[format_t::surr51 - 1].name) + virtual_device_id, + "virtual-"s.append(formats[format_t::surr71 - 1].name) + virtual_device_id, + }); + } + + return sink; } - if(!virtual_device_id.empty()) { - sink.null = std::make_optional(sink_t::null_t { - "virtual-"s.append(formats[format_t::stereo - 1].name) + virtual_device_id, - "virtual-"s.append(formats[format_t::surr51 - 1].name) + virtual_device_id, - "virtual-"s.append(formats[format_t::surr71 - 1].name) + virtual_device_id, - }); + std::unique_ptr + microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override { + auto mic = std::make_unique(); + + if (mic->init(sample_rate, frame_size, channels)) { + return nullptr; + } + + return mic; } - return sink; - } - - std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override { - auto mic = std::make_unique(); - - if(mic->init(sample_rate, frame_size, channels)) { - return nullptr; - } - - return mic; - } - - /** + /** * If the requested sink is a virtual sink, meaning no speakers attached to * the host, then we can seamlessly set the format to stereo and surround sound. * @@ -571,126 +582,132 @@ public: * virtual-(format name) * If it doesn't contain that prefix, then the format will not be changed */ - std::optional set_format(const std::string &sink) { - std::string_view sv { sink.c_str(), sink.size() }; + std::optional + set_format(const std::string &sink) { + std::string_view sv { sink.c_str(), sink.size() }; - format_t::type_e type = format_t::none; - // sink format: - // [virtual-(format name)]device_id - auto prefix = "virtual-"sv; - if(sv.find(prefix) == 0) { - sv = sv.substr(prefix.size(), sv.size() - prefix.size()); + format_t::type_e type = format_t::none; + // sink format: + // [virtual-(format name)]device_id + auto prefix = "virtual-"sv; + if (sv.find(prefix) == 0) { + sv = sv.substr(prefix.size(), sv.size() - prefix.size()); - for(auto &format : formats) { - auto &name = format.name; - if(sv.find(name) == 0) { - type = format.type; - sv = sv.substr(name.size(), sv.size() - name.size()); + for (auto &format : formats) { + auto &name = format.name; + if (sv.find(name) == 0) { + type = format.type; + sv = sv.substr(name.size(), sv.size() - name.size()); - break; + break; + } } } - } - auto wstring_device_id = converter.from_bytes(sv.data()); + auto wstring_device_id = converter.from_bytes(sv.data()); + + if (type == format_t::none) { + // wstring_device_id does not contain virtual-(format name) + // It's a simple deviceId, just pass it back + return std::make_optional(std::move(wstring_device_id)); + } + + wave_format_t wave_format; + auto status = policy->GetMixFormat(wstring_device_id.c_str(), &wave_format); + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't acquire Wave Format [0x"sv << util::hex(status).to_string_view() << ']'; + + return std::nullopt; + } + + set_wave_format(wave_format, formats[(int) type - 1]); + + WAVEFORMATEXTENSIBLE p {}; + status = policy->SetDeviceFormat(wstring_device_id.c_str(), wave_format.get(), (WAVEFORMATEX *) &p); + + // Surround 5.1 might contain side-{left, right} instead of speaker in the back + // Try again with different speaker mask. + if (status == 0x88890008 && type == format_t::surr51) { + set_wave_format(wave_format, surround_51_side_speakers); + status = policy->SetDeviceFormat(wstring_device_id.c_str(), wave_format.get(), (WAVEFORMATEX *) &p); + } + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't set Wave Format [0x"sv << util::hex(status).to_string_view() << ']'; + + return std::nullopt; + } - if(type == format_t::none) { - // wstring_device_id does not contain virtual-(format name) - // It's a simple deviceId, just pass it back return std::make_optional(std::move(wstring_device_id)); } - wave_format_t wave_format; - auto status = policy->GetMixFormat(wstring_device_id.c_str(), &wave_format); - if(FAILED(status)) { - BOOST_LOG(error) << "Couldn't acquire Wave Format [0x"sv << util::hex(status).to_string_view() << ']'; - - return std::nullopt; - } - - set_wave_format(wave_format, formats[(int)type - 1]); - - WAVEFORMATEXTENSIBLE p {}; - status = policy->SetDeviceFormat(wstring_device_id.c_str(), wave_format.get(), (WAVEFORMATEX *)&p); - - // Surround 5.1 might contain side-{left, right} instead of speaker in the back - // Try again with different speaker mask. - if(status == 0x88890008 && type == format_t::surr51) { - set_wave_format(wave_format, surround_51_side_speakers); - status = policy->SetDeviceFormat(wstring_device_id.c_str(), wave_format.get(), (WAVEFORMATEX *)&p); - } - if(FAILED(status)) { - BOOST_LOG(error) << "Couldn't set Wave Format [0x"sv << util::hex(status).to_string_view() << ']'; - - return std::nullopt; - } - - return std::make_optional(std::move(wstring_device_id)); - } - - int set_sink(const std::string &sink) override { - auto wstring_device_id = set_format(sink); - if(!wstring_device_id) { - return -1; - } - - int failure {}; - for(int x = 0; x < (int)ERole_enum_count; ++x) { - auto status = policy->SetDefaultEndpoint(wstring_device_id->c_str(), (ERole)x); - if(status) { - BOOST_LOG(warning) << "Couldn't set ["sv << sink << "] to role ["sv << x << ']'; - - ++failure; + int + set_sink(const std::string &sink) override { + auto wstring_device_id = set_format(sink); + if (!wstring_device_id) { + return -1; } + + int failure {}; + for (int x = 0; x < (int) ERole_enum_count; ++x) { + auto status = policy->SetDefaultEndpoint(wstring_device_id->c_str(), (ERole) x); + if (status) { + BOOST_LOG(warning) << "Couldn't set ["sv << sink << "] to role ["sv << x << ']'; + + ++failure; + } + } + + return failure; } - return failure; - } + int + init() { + auto status = CoCreateInstance( + CLSID_CPolicyConfigClient, + nullptr, + CLSCTX_ALL, + IID_IPolicyConfig, + (void **) &policy); - int init() { - auto status = CoCreateInstance( - CLSID_CPolicyConfigClient, - nullptr, - CLSCTX_ALL, - IID_IPolicyConfig, - (void **)&policy); + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't create audio policy config: [0x"sv << util::hex(status).to_string_view() << ']'; - if(FAILED(status)) { - BOOST_LOG(error) << "Couldn't create audio policy config: [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } - return -1; + return 0; } - return 0; - } + ~audio_control_t() override {} - ~audio_control_t() override {} - - policy_t policy; -}; -} // namespace platf::audio + policy_t policy; + }; +} // namespace platf::audio namespace platf { -// It's not big enough to justify it's own source file :/ -namespace dxgi { -int init(); -} - -std::unique_ptr audio_control() { - auto control = std::make_unique(); - - if(control->init()) { - return nullptr; + // It's not big enough to justify it's own source file :/ + namespace dxgi { + int + init(); } - return control; -} + std::unique_ptr + audio_control() { + auto control = std::make_unique(); -std::unique_ptr init() { - if(dxgi::init()) { - return nullptr; + if (control->init()) { + return nullptr; + } + + return control; } - return std::make_unique(); -} -} // namespace platf + + std::unique_ptr + init() { + if (dxgi::init()) { + return nullptr; + } + return std::make_unique(); + } +} // namespace platf diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index dac853fc..ba6aac38 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -16,196 +16,229 @@ #include "src/utility.h" namespace platf::dxgi { -extern const char *format_str[]; + extern const char *format_str[]; -// Add D3D11_CREATE_DEVICE_DEBUG here to enable the D3D11 debug runtime. -// You should have a debugger like WinDbg attached to receive debug messages. -auto constexpr D3D11_CREATE_DEVICE_FLAGS = D3D11_CREATE_DEVICE_VIDEO_SUPPORT; + // Add D3D11_CREATE_DEVICE_DEBUG here to enable the D3D11 debug runtime. + // You should have a debugger like WinDbg attached to receive debug messages. + auto constexpr D3D11_CREATE_DEVICE_FLAGS = D3D11_CREATE_DEVICE_VIDEO_SUPPORT; -template -void Release(T *dxgi) { - dxgi->Release(); -} - -using factory1_t = util::safe_ptr>; -using dxgi_t = util::safe_ptr>; -using dxgi1_t = util::safe_ptr>; -using device_t = util::safe_ptr>; -using device1_t = util::safe_ptr>; -using device_ctx_t = util::safe_ptr>; -using adapter_t = util::safe_ptr>; -using output_t = util::safe_ptr>; -using output1_t = util::safe_ptr>; -using output5_t = util::safe_ptr>; -using output6_t = util::safe_ptr>; -using dup_t = util::safe_ptr>; -using texture2d_t = util::safe_ptr>; -using texture1d_t = util::safe_ptr>; -using resource_t = util::safe_ptr>; -using resource1_t = util::safe_ptr>; -using multithread_t = util::safe_ptr>; -using vs_t = util::safe_ptr>; -using ps_t = util::safe_ptr>; -using blend_t = util::safe_ptr>; -using input_layout_t = util::safe_ptr>; -using render_target_t = util::safe_ptr>; -using shader_res_t = util::safe_ptr>; -using buf_t = util::safe_ptr>; -using raster_state_t = util::safe_ptr>; -using sampler_state_t = util::safe_ptr>; -using blob_t = util::safe_ptr>; -using depth_stencil_state_t = util::safe_ptr>; -using depth_stencil_view_t = util::safe_ptr>; -using keyed_mutex_t = util::safe_ptr>; - -namespace video { -using device_t = util::safe_ptr>; -using ctx_t = util::safe_ptr>; -using processor_t = util::safe_ptr>; -using processor_out_t = util::safe_ptr>; -using processor_in_t = util::safe_ptr>; -using processor_enum_t = util::safe_ptr>; -} // namespace video - -class hwdevice_t; -struct cursor_t { - std::vector img_data; - - DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info; - int x, y; - bool visible; -}; - -class gpu_cursor_t { -public: - gpu_cursor_t() : cursor_view { 0, 0, 0, 0, 0.0f, 1.0f } {}; - void set_pos(LONG rel_x, LONG rel_y, bool visible) { - cursor_view.TopLeftX = rel_x; - cursor_view.TopLeftY = rel_y; - - this->visible = visible; + template + void + Release(T *dxgi) { + dxgi->Release(); } - void set_texture(LONG width, LONG height, texture2d_t &&texture) { - cursor_view.Width = width; - cursor_view.Height = height; + using factory1_t = util::safe_ptr>; + using dxgi_t = util::safe_ptr>; + using dxgi1_t = util::safe_ptr>; + using device_t = util::safe_ptr>; + using device1_t = util::safe_ptr>; + using device_ctx_t = util::safe_ptr>; + using adapter_t = util::safe_ptr>; + using output_t = util::safe_ptr>; + using output1_t = util::safe_ptr>; + using output5_t = util::safe_ptr>; + using output6_t = util::safe_ptr>; + using dup_t = util::safe_ptr>; + using texture2d_t = util::safe_ptr>; + using texture1d_t = util::safe_ptr>; + using resource_t = util::safe_ptr>; + using resource1_t = util::safe_ptr>; + using multithread_t = util::safe_ptr>; + using vs_t = util::safe_ptr>; + using ps_t = util::safe_ptr>; + using blend_t = util::safe_ptr>; + using input_layout_t = util::safe_ptr>; + using render_target_t = util::safe_ptr>; + using shader_res_t = util::safe_ptr>; + using buf_t = util::safe_ptr>; + using raster_state_t = util::safe_ptr>; + using sampler_state_t = util::safe_ptr>; + using blob_t = util::safe_ptr>; + using depth_stencil_state_t = util::safe_ptr>; + using depth_stencil_view_t = util::safe_ptr>; + using keyed_mutex_t = util::safe_ptr>; - this->texture = std::move(texture); - } + namespace video { + using device_t = util::safe_ptr>; + using ctx_t = util::safe_ptr>; + using processor_t = util::safe_ptr>; + using processor_out_t = util::safe_ptr>; + using processor_in_t = util::safe_ptr>; + using processor_enum_t = util::safe_ptr>; + } // namespace video - texture2d_t texture; - shader_res_t input_res; + class hwdevice_t; + struct cursor_t { + std::vector img_data; - D3D11_VIEWPORT cursor_view; + DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info; + int x, y; + bool visible; + }; - bool visible; -}; + class gpu_cursor_t { + public: + gpu_cursor_t(): + cursor_view { 0, 0, 0, 0, 0.0f, 1.0f } {}; + void + set_pos(LONG rel_x, LONG rel_y, bool visible) { + cursor_view.TopLeftX = rel_x; + cursor_view.TopLeftY = rel_y; -class duplication_t { -public: - dup_t dup; - bool has_frame {}; - bool use_dwmflush {}; + this->visible = visible; + } - capture_e next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p); - capture_e reset(dup_t::pointer dup_p = dup_t::pointer()); - capture_e release_frame(); + void + set_texture(LONG width, LONG height, texture2d_t &&texture) { + cursor_view.Width = width; + cursor_view.Height = height; - ~duplication_t(); -}; + this->texture = std::move(texture); + } -class display_base_t : public display_t { -public: - int init(const ::video::config_t &config, const std::string &display_name); - capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override; + texture2d_t texture; + shader_res_t input_res; - std::chrono::nanoseconds delay; + D3D11_VIEWPORT cursor_view; - factory1_t factory; - adapter_t adapter; - output_t output; - device_t device; - device_ctx_t device_ctx; - duplication_t dup; + bool visible; + }; - DXGI_FORMAT capture_format; - D3D_FEATURE_LEVEL feature_level; + class duplication_t { + public: + dup_t dup; + bool has_frame {}; + bool use_dwmflush {}; - typedef enum _D3DKMT_SCHEDULINGPRIORITYCLASS { - D3DKMT_SCHEDULINGPRIORITYCLASS_IDLE, - D3DKMT_SCHEDULINGPRIORITYCLASS_BELOW_NORMAL, - D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL, - D3DKMT_SCHEDULINGPRIORITYCLASS_ABOVE_NORMAL, - D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH, - D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME - } D3DKMT_SCHEDULINGPRIORITYCLASS; + capture_e + next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p); + capture_e + reset(dup_t::pointer dup_p = dup_t::pointer()); + capture_e + release_frame(); - typedef NTSTATUS WINAPI (*PD3DKMTSetProcessSchedulingPriorityClass)(HANDLE, D3DKMT_SCHEDULINGPRIORITYCLASS); + ~duplication_t(); + }; - virtual bool is_hdr() override; - virtual bool get_hdr_metadata(SS_HDR_METADATA &metadata) override; + class display_base_t: public display_t { + public: + int + init(const ::video::config_t &config, const std::string &display_name); + capture_e + capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr img, bool *cursor) override; -protected: - int get_pixel_pitch() { - return (capture_format == DXGI_FORMAT_R16G16B16A16_FLOAT) ? 8 : 4; - } + std::chrono::nanoseconds delay; - const char *dxgi_format_to_string(DXGI_FORMAT format); - const char *colorspace_to_string(DXGI_COLOR_SPACE_TYPE type); + factory1_t factory; + adapter_t adapter; + output_t output; + device_t device; + device_ctx_t device_ctx; + duplication_t dup; - virtual capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) = 0; - virtual int complete_img(img_t *img, bool dummy) = 0; - virtual std::vector get_supported_sdr_capture_formats() = 0; - virtual std::vector get_supported_hdr_capture_formats() = 0; -}; + DXGI_FORMAT capture_format; + D3D_FEATURE_LEVEL feature_level; -class display_ram_t : public display_base_t { -public: - virtual capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override; + typedef enum _D3DKMT_SCHEDULINGPRIORITYCLASS { + D3DKMT_SCHEDULINGPRIORITYCLASS_IDLE, + D3DKMT_SCHEDULINGPRIORITYCLASS_BELOW_NORMAL, + D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL, + D3DKMT_SCHEDULINGPRIORITYCLASS_ABOVE_NORMAL, + D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH, + D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME + } D3DKMT_SCHEDULINGPRIORITYCLASS; - std::shared_ptr alloc_img() override; - int dummy_img(img_t *img) override; - int complete_img(img_t *img, bool dummy) override; - std::vector get_supported_sdr_capture_formats() override; - std::vector get_supported_hdr_capture_formats() override; + typedef NTSTATUS WINAPI (*PD3DKMTSetProcessSchedulingPriorityClass)(HANDLE, D3DKMT_SCHEDULINGPRIORITYCLASS); - int init(const ::video::config_t &config, const std::string &display_name); + virtual bool + is_hdr() override; + virtual bool + get_hdr_metadata(SS_HDR_METADATA &metadata) override; - cursor_t cursor; - D3D11_MAPPED_SUBRESOURCE img_info; - texture2d_t texture; -}; + protected: + int + get_pixel_pitch() { + return (capture_format == DXGI_FORMAT_R16G16B16A16_FLOAT) ? 8 : 4; + } -class display_vram_t : public display_base_t, public std::enable_shared_from_this { -public: - virtual capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override; + const char * + dxgi_format_to_string(DXGI_FORMAT format); + const char * + colorspace_to_string(DXGI_COLOR_SPACE_TYPE type); - std::shared_ptr alloc_img() override; - int dummy_img(img_t *img_base) override; - int complete_img(img_t *img_base, bool dummy) override; - std::vector get_supported_sdr_capture_formats() override; - std::vector get_supported_hdr_capture_formats() override; + virtual capture_e + snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) = 0; + virtual int + complete_img(img_t *img, bool dummy) = 0; + virtual std::vector + get_supported_sdr_capture_formats() = 0; + virtual std::vector + get_supported_hdr_capture_formats() = 0; + }; - int init(const ::video::config_t &config, const std::string &display_name); + class display_ram_t: public display_base_t { + public: + virtual capture_e + snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override; - std::shared_ptr make_hwdevice(pix_fmt_e pix_fmt) override; + std::shared_ptr + alloc_img() override; + int + dummy_img(img_t *img) override; + int + complete_img(img_t *img, bool dummy) override; + std::vector + get_supported_sdr_capture_formats() override; + std::vector + get_supported_hdr_capture_formats() override; - sampler_state_t sampler_linear; + int + init(const ::video::config_t &config, const std::string &display_name); - blend_t blend_alpha; - blend_t blend_invert; - blend_t blend_disable; + cursor_t cursor; + D3D11_MAPPED_SUBRESOURCE img_info; + texture2d_t texture; + }; - ps_t scene_ps; - vs_t scene_vs; + class display_vram_t: public display_base_t, public std::enable_shared_from_this { + public: + virtual capture_e + snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible) override; - gpu_cursor_t cursor_alpha; - gpu_cursor_t cursor_xor; + std::shared_ptr + alloc_img() override; + int + dummy_img(img_t *img_base) override; + int + complete_img(img_t *img_base, bool dummy) override; + std::vector + get_supported_sdr_capture_formats() override; + std::vector + get_supported_hdr_capture_formats() override; - texture2d_t last_frame_copy; + int + init(const ::video::config_t &config, const std::string &display_name); - std::atomic next_image_id; -}; -} // namespace platf::dxgi + std::shared_ptr + make_hwdevice(pix_fmt_e pix_fmt) override; + + sampler_state_t sampler_linear; + + blend_t blend_alpha; + blend_t blend_invert; + blend_t blend_disable; + + ps_t scene_ps; + vs_t scene_vs; + + gpu_cursor_t cursor_alpha; + gpu_cursor_t cursor_xor; + + texture2d_t last_frame_copy; + + std::atomic next_image_id; + }; +} // namespace platf::dxgi #endif diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 39be1112..f6c3bfb3 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -20,894 +20,908 @@ typedef long NTSTATUS; #include "src/video.h" namespace platf { -using namespace std::literals; + using namespace std::literals; } namespace platf::dxgi { -namespace bp = boost::process; + namespace bp = boost::process; + + capture_e + duplication_t::next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p) { + auto capture_status = release_frame(); + if (capture_status != capture_e::ok) { + return capture_status; + } + + if (use_dwmflush) { + DwmFlush(); + } + + auto status = dup->AcquireNextFrame(timeout.count(), &frame_info, res_p); + + switch (status) { + case S_OK: + has_frame = true; + return capture_e::ok; + case DXGI_ERROR_WAIT_TIMEOUT: + return capture_e::timeout; + case WAIT_ABANDONED: + case DXGI_ERROR_ACCESS_LOST: + case DXGI_ERROR_ACCESS_DENIED: + return capture_e::reinit; + default: + BOOST_LOG(error) << "Couldn't acquire next frame [0x"sv << util::hex(status).to_string_view(); + return capture_e::error; + } + } + + capture_e + duplication_t::reset(dup_t::pointer dup_p) { + auto capture_status = release_frame(); + + dup.reset(dup_p); -capture_e duplication_t::next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p) { - auto capture_status = release_frame(); - if(capture_status != capture_e::ok) { return capture_status; } - if(use_dwmflush) { - DwmFlush(); + capture_e + duplication_t::release_frame() { + if (!has_frame) { + return capture_e::ok; + } + + auto status = dup->ReleaseFrame(); + switch (status) { + case S_OK: + has_frame = false; + return capture_e::ok; + case DXGI_ERROR_WAIT_TIMEOUT: + return capture_e::timeout; + case WAIT_ABANDONED: + case DXGI_ERROR_ACCESS_LOST: + case DXGI_ERROR_ACCESS_DENIED: + has_frame = false; + return capture_e::reinit; + default: + BOOST_LOG(error) << "Couldn't release frame [0x"sv << util::hex(status).to_string_view(); + return capture_e::error; + } } - auto status = dup->AcquireNextFrame(timeout.count(), &frame_info, res_p); - - switch(status) { - case S_OK: - has_frame = true; - return capture_e::ok; - case DXGI_ERROR_WAIT_TIMEOUT: - return capture_e::timeout; - case WAIT_ABANDONED: - case DXGI_ERROR_ACCESS_LOST: - case DXGI_ERROR_ACCESS_DENIED: - return capture_e::reinit; - default: - BOOST_LOG(error) << "Couldn't acquire next frame [0x"sv << util::hex(status).to_string_view(); - return capture_e::error; + duplication_t::~duplication_t() { + release_frame(); } -} -capture_e duplication_t::reset(dup_t::pointer dup_p) { - auto capture_status = release_frame(); + capture_e + display_base_t::capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<::platf::img_t> img, bool *cursor) { + auto next_frame = std::chrono::steady_clock::now(); - dup.reset(dup_p); + // Use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION if supported (Windows 10 1809+) + HANDLE timer = CreateWaitableTimerEx(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); + if (!timer) { + timer = CreateWaitableTimerEx(nullptr, nullptr, 0, TIMER_ALL_ACCESS); + if (!timer) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to create timer: "sv << winerr; + return capture_e::error; + } + } - return capture_status; -} + auto close_timer = util::fail_guard([timer]() { + CloseHandle(timer); + }); + + while (img) { + // This will return false if the HDR state changes or for any number of other + // display or GPU changes. We should reinit to examine the updated state of + // the display subsystem. It is recommended to call this once per frame. + if (!factory->IsCurrent()) { + return platf::capture_e::reinit; + } + + // If the wait time is between 1 us and 1 second, wait the specified time + // and offset the next frame time from the exact current frame time target. + auto wait_time_us = std::chrono::duration_cast(next_frame - std::chrono::steady_clock::now()).count(); + if (wait_time_us > 0 && wait_time_us < 1000000) { + LARGE_INTEGER due_time { .QuadPart = -10LL * wait_time_us }; + SetWaitableTimer(timer, &due_time, 0, nullptr, nullptr, false); + WaitForSingleObject(timer, INFINITE); + next_frame += delay; + } + else { + // If the wait time is negative (meaning the frame is past due) or the + // computed wait time is beyond a second (meaning possible clock issues), + // just capture the frame now and resynchronize the frame interval with + // the current time. + next_frame = std::chrono::steady_clock::now() + delay; + } + + auto status = snapshot(img.get(), 1000ms, *cursor); + switch (status) { + case platf::capture_e::reinit: + case platf::capture_e::error: + return status; + case platf::capture_e::timeout: + img = snapshot_cb(img, false); + break; + case platf::capture_e::ok: + img = snapshot_cb(img, true); + break; + default: + BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']'; + return status; + } + } -capture_e duplication_t::release_frame() { - if(!has_frame) { return capture_e::ok; } - auto status = dup->ReleaseFrame(); - switch(status) { - case S_OK: - has_frame = false; - return capture_e::ok; - case DXGI_ERROR_WAIT_TIMEOUT: - return capture_e::timeout; - case WAIT_ABANDONED: - case DXGI_ERROR_ACCESS_LOST: - case DXGI_ERROR_ACCESS_DENIED: - has_frame = false; - return capture_e::reinit; - default: - BOOST_LOG(error) << "Couldn't release frame [0x"sv << util::hex(status).to_string_view(); - return capture_e::error; - } -} + bool + set_gpu_preference_on_self(int preference) { + // The GPU preferences key uses app path as the value name. + WCHAR sunshine_path[MAX_PATH]; + GetModuleFileNameW(NULL, sunshine_path, ARRAYSIZE(sunshine_path)); -duplication_t::~duplication_t() { - release_frame(); -} + WCHAR value_data[128]; + swprintf_s(value_data, L"GpuPreference=%d;", preference); -capture_e display_base_t::capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<::platf::img_t> img, bool *cursor) { - auto next_frame = std::chrono::steady_clock::now(); - - // Use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION if supported (Windows 10 1809+) - HANDLE timer = CreateWaitableTimerEx(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); - if(!timer) { - timer = CreateWaitableTimerEx(nullptr, nullptr, 0, TIMER_ALL_ACCESS); - if(!timer) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to create timer: "sv << winerr; - return capture_e::error; - } - } - - auto close_timer = util::fail_guard([timer]() { - CloseHandle(timer); - }); - - while(img) { - // This will return false if the HDR state changes or for any number of other - // display or GPU changes. We should reinit to examine the updated state of - // the display subsystem. It is recommended to call this once per frame. - if(!factory->IsCurrent()) { - return platf::capture_e::reinit; - } - - // If the wait time is between 1 us and 1 second, wait the specified time - // and offset the next frame time from the exact current frame time target. - auto wait_time_us = std::chrono::duration_cast(next_frame - std::chrono::steady_clock::now()).count(); - if(wait_time_us > 0 && wait_time_us < 1000000) { - LARGE_INTEGER due_time { .QuadPart = -10LL * wait_time_us }; - SetWaitableTimer(timer, &due_time, 0, nullptr, nullptr, false); - WaitForSingleObject(timer, INFINITE); - next_frame += delay; - } - else { - // If the wait time is negative (meaning the frame is past due) or the - // computed wait time is beyond a second (meaning possible clock issues), - // just capture the frame now and resynchronize the frame interval with - // the current time. - next_frame = std::chrono::steady_clock::now() + delay; - } - - auto status = snapshot(img.get(), 1000ms, *cursor); - switch(status) { - case platf::capture_e::reinit: - case platf::capture_e::error: - return status; - case platf::capture_e::timeout: - img = snapshot_cb(img, false); - break; - case platf::capture_e::ok: - img = snapshot_cb(img, true); - break; - default: - BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']'; - return status; - } - } - - return capture_e::ok; -} - -bool set_gpu_preference_on_self(int preference) { - // The GPU preferences key uses app path as the value name. - WCHAR sunshine_path[MAX_PATH]; - GetModuleFileNameW(NULL, sunshine_path, ARRAYSIZE(sunshine_path)); - - WCHAR value_data[128]; - swprintf_s(value_data, L"GpuPreference=%d;", preference); - - auto status = RegSetKeyValueW(HKEY_CURRENT_USER, - L"Software\\Microsoft\\DirectX\\UserGpuPreferences", - sunshine_path, - REG_SZ, - value_data, - (wcslen(value_data) + 1) * sizeof(WCHAR)); - if(status != ERROR_SUCCESS) { - BOOST_LOG(error) << "Failed to set GPU preference: "sv << status; - return false; - } - - BOOST_LOG(info) << "Set GPU preference: "sv << preference; - return true; -} - -// On hybrid graphics systems, Windows will change the order of GPUs reported by -// DXGI in accordance with the user's GPU preference. If the selected GPU is a -// render-only device with no displays, DXGI will add virtual outputs to the -// that device to avoid confusing applications. While this works properly for most -// applications, it breaks the Desktop Duplication API because DXGI doesn't proxy -// the virtual DXGIOutput to the real GPU it is attached to. When trying to call -// DuplicateOutput() on one of these virtual outputs, it fails with DXGI_ERROR_UNSUPPORTED -// (even if you try sneaky stuff like passing the ID3D11Device for the iGPU and the -// virtual DXGIOutput from the dGPU). Because the GPU preference is once-per-process, -// we spawn a helper tool to probe for us before we set our own GPU preference. -bool probe_for_gpu_preference(const std::string &display_name) { - // If we've already been through here, there's nothing to do this time. - static bool set_gpu_preference = false; - if(set_gpu_preference) { - return true; - } - - std::string cmd = "tools\\ddprobe.exe"; - - // We start at 1 because 0 is automatic selection which can be overridden by - // the GPU driver control panel options. Since ddprobe.exe can have different - // GPU driver overrides than Sunshine.exe, we want to avoid a scenario where - // autoselection might work for ddprobe.exe but not for us. - for(int i = 1; i < 5; i++) { - // Run the probe tool. It returns the status of DuplicateOutput(). - // - // Arg format: [GPU preference] [Display name] - HRESULT result; - try { - result = bp::system(cmd, std::to_string(i), display_name, bp::std_out > bp::null, bp::std_err > bp::null); - } - catch(bp::process_error &e) { - BOOST_LOG(error) << "Failed to start ddprobe.exe: "sv << e.what(); + auto status = RegSetKeyValueW(HKEY_CURRENT_USER, + L"Software\\Microsoft\\DirectX\\UserGpuPreferences", + sunshine_path, + REG_SZ, + value_data, + (wcslen(value_data) + 1) * sizeof(WCHAR)); + if (status != ERROR_SUCCESS) { + BOOST_LOG(error) << "Failed to set GPU preference: "sv << status; return false; } - BOOST_LOG(info) << "ddprobe.exe ["sv << i << "] ["sv << display_name << "] returned: 0x"sv << util::hex(result).to_string_view(); - - // E_ACCESSDENIED can happen at the login screen. If we get this error, - // we know capture would have been supported, because DXGI_ERROR_UNSUPPORTED - // would have been raised first if it wasn't. - if(result == S_OK || result == E_ACCESSDENIED) { - // We found a working GPU preference, so set ourselves to use that. - if(set_gpu_preference_on_self(i)) { - set_gpu_preference = true; - return true; - } - else { - return false; - } - } - else { - // This configuration didn't work, so continue testing others - continue; - } + BOOST_LOG(info) << "Set GPU preference: "sv << preference; + return true; } - // If none of the manual options worked, leave the GPU preference alone - return false; -} - -bool test_dxgi_duplication(adapter_t &adapter, output_t &output) { - D3D_FEATURE_LEVEL featureLevels[] { - D3D_FEATURE_LEVEL_11_1, - D3D_FEATURE_LEVEL_11_0, - D3D_FEATURE_LEVEL_10_1, - D3D_FEATURE_LEVEL_10_0, - D3D_FEATURE_LEVEL_9_3, - D3D_FEATURE_LEVEL_9_2, - D3D_FEATURE_LEVEL_9_1 - }; - - device_t device; - auto status = D3D11CreateDevice( - adapter.get(), - D3D_DRIVER_TYPE_UNKNOWN, - nullptr, - D3D11_CREATE_DEVICE_FLAGS, - featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), - D3D11_SDK_VERSION, - &device, - nullptr, - nullptr); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create D3D11 device for DD test [0x"sv << util::hex(status).to_string_view() << ']'; - return false; - } - - output1_t output1; - status = output->QueryInterface(IID_IDXGIOutput1, (void **)&output1); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv; - return false; - } - - // Check if we can use the Desktop Duplication API on this output - for(int x = 0; x < 2; ++x) { - dup_t dup; - status = output1->DuplicateOutput((IUnknown *)device.get(), &dup); - if(SUCCEEDED(status)) { + // On hybrid graphics systems, Windows will change the order of GPUs reported by + // DXGI in accordance with the user's GPU preference. If the selected GPU is a + // render-only device with no displays, DXGI will add virtual outputs to the + // that device to avoid confusing applications. While this works properly for most + // applications, it breaks the Desktop Duplication API because DXGI doesn't proxy + // the virtual DXGIOutput to the real GPU it is attached to. When trying to call + // DuplicateOutput() on one of these virtual outputs, it fails with DXGI_ERROR_UNSUPPORTED + // (even if you try sneaky stuff like passing the ID3D11Device for the iGPU and the + // virtual DXGIOutput from the dGPU). Because the GPU preference is once-per-process, + // we spawn a helper tool to probe for us before we set our own GPU preference. + bool + probe_for_gpu_preference(const std::string &display_name) { + // If we've already been through here, there's nothing to do this time. + static bool set_gpu_preference = false; + if (set_gpu_preference) { return true; } - Sleep(200); - } - BOOST_LOG(error) << "DuplicateOutput() test failed [0x"sv << util::hex(status).to_string_view() << ']'; - return false; -} + std::string cmd = "tools\\ddprobe.exe"; -int display_base_t::init(const ::video::config_t &config, const std::string &display_name) { - std::once_flag windows_cpp_once_flag; + // We start at 1 because 0 is automatic selection which can be overridden by + // the GPU driver control panel options. Since ddprobe.exe can have different + // GPU driver overrides than Sunshine.exe, we want to avoid a scenario where + // autoselection might work for ddprobe.exe but not for us. + for (int i = 1; i < 5; i++) { + // Run the probe tool. It returns the status of DuplicateOutput(). + // + // Arg format: [GPU preference] [Display name] + HRESULT result; + try { + result = bp::system(cmd, std::to_string(i), display_name, bp::std_out > bp::null, bp::std_err > bp::null); + } + catch (bp::process_error &e) { + BOOST_LOG(error) << "Failed to start ddprobe.exe: "sv << e.what(); + return false; + } - std::call_once(windows_cpp_once_flag, []() { - DECLARE_HANDLE(DPI_AWARENESS_CONTEXT); + BOOST_LOG(info) << "ddprobe.exe ["sv << i << "] ["sv << display_name << "] returned: 0x"sv << util::hex(result).to_string_view(); - typedef BOOL (*User32_SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT value); - - auto user32 = LoadLibraryA("user32.dll"); - auto f = (User32_SetProcessDpiAwarenessContext)GetProcAddress(user32, "SetProcessDpiAwarenessContext"); - if(f) { - f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + // E_ACCESSDENIED can happen at the login screen. If we get this error, + // we know capture would have been supported, because DXGI_ERROR_UNSUPPORTED + // would have been raised first if it wasn't. + if (result == S_OK || result == E_ACCESSDENIED) { + // We found a working GPU preference, so set ourselves to use that. + if (set_gpu_preference_on_self(i)) { + set_gpu_preference = true; + return true; + } + else { + return false; + } + } + else { + // This configuration didn't work, so continue testing others + continue; + } } - FreeLibrary(user32); - }); - - // Ensure we can duplicate the current display - syncThreadDesktop(); - - delay = std::chrono::nanoseconds { 1s } / config.framerate; - - // Get rectangle of full desktop for absolute mouse coordinates - env_width = GetSystemMetrics(SM_CXVIRTUALSCREEN); - env_height = GetSystemMetrics(SM_CYVIRTUALSCREEN); - - HRESULT status; - - // We must set the GPU preference before calling any DXGI APIs! - if(!probe_for_gpu_preference(display_name)) { - BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; + // If none of the manual options worked, leave the GPU preference alone + return false; } - status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **)&factory); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } + bool + test_dxgi_duplication(adapter_t &adapter, output_t &output) { + D3D_FEATURE_LEVEL featureLevels[] { + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL_9_1 + }; - std::wstring_convert, wchar_t> converter; - - auto adapter_name = converter.from_bytes(config::video.adapter_name); - auto output_name = converter.from_bytes(display_name); - - adapter_t::pointer adapter_p; - for(int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) { - dxgi::adapter_t adapter_tmp { adapter_p }; - - DXGI_ADAPTER_DESC1 adapter_desc; - adapter_tmp->GetDesc1(&adapter_desc); - - if(!adapter_name.empty() && adapter_desc.Description != adapter_name) { - continue; + device_t device; + auto status = D3D11CreateDevice( + adapter.get(), + D3D_DRIVER_TYPE_UNKNOWN, + nullptr, + D3D11_CREATE_DEVICE_FLAGS, + featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), + D3D11_SDK_VERSION, + &device, + nullptr, + nullptr); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create D3D11 device for DD test [0x"sv << util::hex(status).to_string_view() << ']'; + return false; } - dxgi::output_t::pointer output_p; - for(int y = 0; adapter_tmp->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) { - dxgi::output_t output_tmp { output_p }; + output1_t output1; + status = output->QueryInterface(IID_IDXGIOutput1, (void **) &output1); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv; + return false; + } - DXGI_OUTPUT_DESC desc; - output_tmp->GetDesc(&desc); + // Check if we can use the Desktop Duplication API on this output + for (int x = 0; x < 2; ++x) { + dup_t dup; + status = output1->DuplicateOutput((IUnknown *) device.get(), &dup); + if (SUCCEEDED(status)) { + return true; + } + Sleep(200); + } - if(!output_name.empty() && desc.DeviceName != output_name) { + BOOST_LOG(error) << "DuplicateOutput() test failed [0x"sv << util::hex(status).to_string_view() << ']'; + return false; + } + + int + display_base_t::init(const ::video::config_t &config, const std::string &display_name) { + std::once_flag windows_cpp_once_flag; + + std::call_once(windows_cpp_once_flag, []() { + DECLARE_HANDLE(DPI_AWARENESS_CONTEXT); + + typedef BOOL (*User32_SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT value); + + auto user32 = LoadLibraryA("user32.dll"); + auto f = (User32_SetProcessDpiAwarenessContext) GetProcAddress(user32, "SetProcessDpiAwarenessContext"); + if (f) { + f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + } + + FreeLibrary(user32); + }); + + // Ensure we can duplicate the current display + syncThreadDesktop(); + + delay = std::chrono::nanoseconds { 1s } / config.framerate; + + // Get rectangle of full desktop for absolute mouse coordinates + env_width = GetSystemMetrics(SM_CXVIRTUALSCREEN); + env_height = GetSystemMetrics(SM_CYVIRTUALSCREEN); + + HRESULT status; + + // We must set the GPU preference before calling any DXGI APIs! + if (!probe_for_gpu_preference(display_name)) { + BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; + } + + status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + std::wstring_convert, wchar_t> converter; + + auto adapter_name = converter.from_bytes(config::video.adapter_name); + auto output_name = converter.from_bytes(display_name); + + adapter_t::pointer adapter_p; + for (int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) { + dxgi::adapter_t adapter_tmp { adapter_p }; + + DXGI_ADAPTER_DESC1 adapter_desc; + adapter_tmp->GetDesc1(&adapter_desc); + + if (!adapter_name.empty() && adapter_desc.Description != adapter_name) { continue; } - if(desc.AttachedToDesktop && test_dxgi_duplication(adapter_tmp, output_tmp)) { - output = std::move(output_tmp); + dxgi::output_t::pointer output_p; + for (int y = 0; adapter_tmp->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) { + dxgi::output_t output_tmp { output_p }; - offset_x = desc.DesktopCoordinates.left; - offset_y = desc.DesktopCoordinates.top; - width = desc.DesktopCoordinates.right - offset_x; - height = desc.DesktopCoordinates.bottom - offset_y; + DXGI_OUTPUT_DESC desc; + output_tmp->GetDesc(&desc); - // left and bottom may be negative, yet absolute mouse coordinates start at 0x0 - // Ensure offset starts at 0x0 - offset_x -= GetSystemMetrics(SM_XVIRTUALSCREEN); - offset_y -= GetSystemMetrics(SM_YVIRTUALSCREEN); - } - } + if (!output_name.empty() && desc.DeviceName != output_name) { + continue; + } - if(output) { - adapter = std::move(adapter_tmp); - break; - } - } + if (desc.AttachedToDesktop && test_dxgi_duplication(adapter_tmp, output_tmp)) { + output = std::move(output_tmp); - if(!output) { - BOOST_LOG(error) << "Failed to locate an output device"sv; - return -1; - } + offset_x = desc.DesktopCoordinates.left; + offset_y = desc.DesktopCoordinates.top; + width = desc.DesktopCoordinates.right - offset_x; + height = desc.DesktopCoordinates.bottom - offset_y; - D3D_FEATURE_LEVEL featureLevels[] { - D3D_FEATURE_LEVEL_11_1, - D3D_FEATURE_LEVEL_11_0, - D3D_FEATURE_LEVEL_10_1, - D3D_FEATURE_LEVEL_10_0, - D3D_FEATURE_LEVEL_9_3, - D3D_FEATURE_LEVEL_9_2, - D3D_FEATURE_LEVEL_9_1 - }; - - status = adapter->QueryInterface(IID_IDXGIAdapter, (void **)&adapter_p); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to query IDXGIAdapter interface"sv; - return -1; - } - - status = D3D11CreateDevice( - adapter_p, - D3D_DRIVER_TYPE_UNKNOWN, - nullptr, - D3D11_CREATE_DEVICE_FLAGS, - featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), - D3D11_SDK_VERSION, - &device, - &feature_level, - &device_ctx); - - adapter_p->Release(); - - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create D3D11 device [0x"sv << util::hex(status).to_string_view() << ']'; - - return -1; - } - - DXGI_ADAPTER_DESC adapter_desc; - adapter->GetDesc(&adapter_desc); - - auto description = converter.to_bytes(adapter_desc.Description); - BOOST_LOG(info) - << std::endl - << "Device Description : " << description << std::endl - << "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl - << "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl - << "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl - << "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl - << "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl - << "Feature Level : 0x"sv << util::hex(feature_level).to_string_view() << std::endl - << "Capture size : "sv << width << 'x' << height << std::endl - << "Offset : "sv << offset_x << 'x' << offset_y << std::endl - << "Virtual Desktop : "sv << env_width << 'x' << env_height; - - // Enable DwmFlush() only if the current refresh rate can match the client framerate. - auto refresh_rate = config.framerate; - DWM_TIMING_INFO timing_info; - timing_info.cbSize = sizeof(timing_info); - - status = DwmGetCompositionTimingInfo(NULL, &timing_info); - if(FAILED(status)) { - BOOST_LOG(warning) << "Failed to detect active refresh rate."; - } - else { - refresh_rate = std::round((double)timing_info.rateRefresh.uiNumerator / (double)timing_info.rateRefresh.uiDenominator); - } - - dup.use_dwmflush = config::video.dwmflush && !(config.framerate > refresh_rate) ? true : false; - - // Bump up thread priority - { - const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; - TOKEN_PRIVILEGES tp; - HANDLE token; - LUID val; - - if(OpenProcessToken(GetCurrentProcess(), flags, &token) && - !!LookupPrivilegeValue(NULL, SE_INC_BASE_PRIORITY_NAME, &val)) { - tp.PrivilegeCount = 1; - tp.Privileges[0].Luid = val; - tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - - if(!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL)) { - BOOST_LOG(warning) << "Could not set privilege to increase GPU priority"; - } - } - - CloseHandle(token); - - HMODULE gdi32 = GetModuleHandleA("GDI32"); - if(gdi32) { - PD3DKMTSetProcessSchedulingPriorityClass fn = - (PD3DKMTSetProcessSchedulingPriorityClass)GetProcAddress(gdi32, "D3DKMTSetProcessSchedulingPriorityClass"); - if(fn) { - status = fn(GetCurrentProcess(), D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME); - if(FAILED(status)) { - BOOST_LOG(warning) << "Failed to set realtime GPU priority. Please run application as administrator for optimal performance."; + // left and bottom may be negative, yet absolute mouse coordinates start at 0x0 + // Ensure offset starts at 0x0 + offset_x -= GetSystemMetrics(SM_XVIRTUALSCREEN); + offset_y -= GetSystemMetrics(SM_YVIRTUALSCREEN); } } + + if (output) { + adapter = std::move(adapter_tmp); + break; + } } - dxgi::dxgi_t dxgi; - status = device->QueryInterface(IID_IDXGIDevice, (void **)&dxgi); - if(FAILED(status)) { - BOOST_LOG(warning) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']'; + if (!output) { + BOOST_LOG(error) << "Failed to locate an output device"sv; return -1; } - status = dxgi->SetGPUThreadPriority(7); - if(FAILED(status)) { - BOOST_LOG(warning) << "Failed to increase capture GPU thread priority. Please run application as administrator for optimal performance."; - } - } + D3D_FEATURE_LEVEL featureLevels[] { + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL_9_1 + }; - // Try to reduce latency - { - dxgi::dxgi1_t dxgi {}; - status = device->QueryInterface(IID_IDXGIDevice, (void **)&dxgi); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']'; + status = adapter->QueryInterface(IID_IDXGIAdapter, (void **) &adapter_p); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to query IDXGIAdapter interface"sv; return -1; } - status = dxgi->SetMaximumFrameLatency(1); - if(FAILED(status)) { - BOOST_LOG(warning) << "Failed to set maximum frame latency [0x"sv << util::hex(status).to_string_view() << ']'; + status = D3D11CreateDevice( + adapter_p, + D3D_DRIVER_TYPE_UNKNOWN, + nullptr, + D3D11_CREATE_DEVICE_FLAGS, + featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), + D3D11_SDK_VERSION, + &device, + &feature_level, + &device_ctx); + + adapter_p->Release(); + + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create D3D11 device [0x"sv << util::hex(status).to_string_view() << ']'; + + return -1; } - } - //FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD - { - // IDXGIOutput5 is optional, but can provide improved performance and wide color support - dxgi::output5_t output5 {}; - status = output->QueryInterface(IID_IDXGIOutput5, (void **)&output5); - if(SUCCEEDED(status)) { - // Ask the display implementation which formats it supports - auto supported_formats = config.dynamicRange ? get_supported_hdr_capture_formats() : get_supported_sdr_capture_formats(); - if(supported_formats.empty()) { - BOOST_LOG(warning) << "No compatible capture formats for this encoder"sv; - return -1; - } + DXGI_ADAPTER_DESC adapter_desc; + adapter->GetDesc(&adapter_desc); - // We try this twice, in case we still get an error on reinitialization - for(int x = 0; x < 2; ++x) { - status = output5->DuplicateOutput1((IUnknown *)device.get(), 0, supported_formats.size(), supported_formats.data(), &dup.dup); - if(SUCCEEDED(status)) { - break; - } - std::this_thread::sleep_for(200ms); - } + auto description = converter.to_bytes(adapter_desc.Description); + BOOST_LOG(info) + << std::endl + << "Device Description : " << description << std::endl + << "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl + << "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl + << "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl + << "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl + << "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl + << "Feature Level : 0x"sv << util::hex(feature_level).to_string_view() << std::endl + << "Capture size : "sv << width << 'x' << height << std::endl + << "Offset : "sv << offset_x << 'x' << offset_y << std::endl + << "Virtual Desktop : "sv << env_width << 'x' << env_height; - // We don't retry with DuplicateOutput() because we can hit this codepath when we're racing - // with mode changes and we don't want to accidentally fall back to suboptimal capture if - // we get unlucky and succeed below. - if(FAILED(status)) { - BOOST_LOG(warning) << "DuplicateOutput1 Failed [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } + // Enable DwmFlush() only if the current refresh rate can match the client framerate. + auto refresh_rate = config.framerate; + DWM_TIMING_INFO timing_info; + timing_info.cbSize = sizeof(timing_info); + + status = DwmGetCompositionTimingInfo(NULL, &timing_info); + if (FAILED(status)) { + BOOST_LOG(warning) << "Failed to detect active refresh rate."; } else { - BOOST_LOG(warning) << "IDXGIOutput5 is not supported by your OS. Capture performance may be reduced."sv; + refresh_rate = std::round((double) timing_info.rateRefresh.uiNumerator / (double) timing_info.rateRefresh.uiDenominator); + } - dxgi::output1_t output1 {}; - status = output->QueryInterface(IID_IDXGIOutput1, (void **)&output1); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv; - return -1; - } + dup.use_dwmflush = config::video.dwmflush && !(config.framerate > refresh_rate) ? true : false; - for(int x = 0; x < 2; ++x) { - status = output1->DuplicateOutput((IUnknown *)device.get(), &dup.dup); - if(SUCCEEDED(status)) { - break; + // Bump up thread priority + { + const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; + TOKEN_PRIVILEGES tp; + HANDLE token; + LUID val; + + if (OpenProcessToken(GetCurrentProcess(), flags, &token) && + !!LookupPrivilegeValue(NULL, SE_INC_BASE_PRIORITY_NAME, &val)) { + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = val; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + if (!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL)) { + BOOST_LOG(warning) << "Could not set privilege to increase GPU priority"; } - std::this_thread::sleep_for(200ms); } - if(FAILED(status)) { - BOOST_LOG(error) << "DuplicateOutput Failed [0x"sv << util::hex(status).to_string_view() << ']'; + CloseHandle(token); + + HMODULE gdi32 = GetModuleHandleA("GDI32"); + if (gdi32) { + PD3DKMTSetProcessSchedulingPriorityClass fn = + (PD3DKMTSetProcessSchedulingPriorityClass) GetProcAddress(gdi32, "D3DKMTSetProcessSchedulingPriorityClass"); + if (fn) { + status = fn(GetCurrentProcess(), D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME); + if (FAILED(status)) { + BOOST_LOG(warning) << "Failed to set realtime GPU priority. Please run application as administrator for optimal performance."; + } + } + } + + dxgi::dxgi_t dxgi; + status = device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi); + if (FAILED(status)) { + BOOST_LOG(warning) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } + + status = dxgi->SetGPUThreadPriority(7); + if (FAILED(status)) { + BOOST_LOG(warning) << "Failed to increase capture GPU thread priority. Please run application as administrator for optimal performance."; + } } + + // Try to reduce latency + { + dxgi::dxgi1_t dxgi {}; + status = device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + status = dxgi->SetMaximumFrameLatency(1); + if (FAILED(status)) { + BOOST_LOG(warning) << "Failed to set maximum frame latency [0x"sv << util::hex(status).to_string_view() << ']'; + } + } + + //FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD + { + // IDXGIOutput5 is optional, but can provide improved performance and wide color support + dxgi::output5_t output5 {}; + status = output->QueryInterface(IID_IDXGIOutput5, (void **) &output5); + if (SUCCEEDED(status)) { + // Ask the display implementation which formats it supports + auto supported_formats = config.dynamicRange ? get_supported_hdr_capture_formats() : get_supported_sdr_capture_formats(); + if (supported_formats.empty()) { + BOOST_LOG(warning) << "No compatible capture formats for this encoder"sv; + return -1; + } + + // We try this twice, in case we still get an error on reinitialization + for (int x = 0; x < 2; ++x) { + status = output5->DuplicateOutput1((IUnknown *) device.get(), 0, supported_formats.size(), supported_formats.data(), &dup.dup); + if (SUCCEEDED(status)) { + break; + } + std::this_thread::sleep_for(200ms); + } + + // We don't retry with DuplicateOutput() because we can hit this codepath when we're racing + // with mode changes and we don't want to accidentally fall back to suboptimal capture if + // we get unlucky and succeed below. + if (FAILED(status)) { + BOOST_LOG(warning) << "DuplicateOutput1 Failed [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + } + else { + BOOST_LOG(warning) << "IDXGIOutput5 is not supported by your OS. Capture performance may be reduced."sv; + + dxgi::output1_t output1 {}; + status = output->QueryInterface(IID_IDXGIOutput1, (void **) &output1); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv; + return -1; + } + + for (int x = 0; x < 2; ++x) { + status = output1->DuplicateOutput((IUnknown *) device.get(), &dup.dup); + if (SUCCEEDED(status)) { + break; + } + std::this_thread::sleep_for(200ms); + } + + if (FAILED(status)) { + BOOST_LOG(error) << "DuplicateOutput Failed [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + } + } + + DXGI_OUTDUPL_DESC dup_desc; + dup.dup->GetDesc(&dup_desc); + + BOOST_LOG(info) << "Desktop resolution ["sv << dup_desc.ModeDesc.Width << 'x' << dup_desc.ModeDesc.Height << ']'; + BOOST_LOG(info) << "Desktop format ["sv << dxgi_format_to_string(dup_desc.ModeDesc.Format) << ']'; + + dxgi::output6_t output6 {}; + status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6); + if (SUCCEEDED(status)) { + DXGI_OUTPUT_DESC1 desc1; + output6->GetDesc1(&desc1); + + BOOST_LOG(info) + << std::endl + << "Colorspace : "sv << colorspace_to_string(desc1.ColorSpace) << std::endl + << "Bits Per Color : "sv << desc1.BitsPerColor << std::endl + << "Red Primary : ["sv << desc1.RedPrimary[0] << ',' << desc1.RedPrimary[1] << ']' << std::endl + << "Green Primary : ["sv << desc1.GreenPrimary[0] << ',' << desc1.GreenPrimary[1] << ']' << std::endl + << "Blue Primary : ["sv << desc1.BluePrimary[0] << ',' << desc1.BluePrimary[1] << ']' << std::endl + << "White Point : ["sv << desc1.WhitePoint[0] << ',' << desc1.WhitePoint[1] << ']' << std::endl + << "Min Luminance : "sv << desc1.MinLuminance << " nits"sv << std::endl + << "Max Luminance : "sv << desc1.MaxLuminance << " nits"sv << std::endl + << "Max Full Luminance : "sv << desc1.MaxFullFrameLuminance << " nits"sv; + } + + // Capture format will be determined from the first call to AcquireNextFrame() + capture_format = DXGI_FORMAT_UNKNOWN; + + return 0; } - DXGI_OUTDUPL_DESC dup_desc; - dup.dup->GetDesc(&dup_desc); + bool + display_base_t::is_hdr() { + dxgi::output6_t output6 {}; - BOOST_LOG(info) << "Desktop resolution ["sv << dup_desc.ModeDesc.Width << 'x' << dup_desc.ModeDesc.Height << ']'; - BOOST_LOG(info) << "Desktop format ["sv << dxgi_format_to_string(dup_desc.ModeDesc.Format) << ']'; + auto status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6); + if (FAILED(status)) { + BOOST_LOG(warning) << "Failed to query IDXGIOutput6 from the output"sv; + return false; + } - dxgi::output6_t output6 {}; - status = output->QueryInterface(IID_IDXGIOutput6, (void **)&output6); - if(SUCCEEDED(status)) { DXGI_OUTPUT_DESC1 desc1; output6->GetDesc1(&desc1); - BOOST_LOG(info) - << std::endl - << "Colorspace : "sv << colorspace_to_string(desc1.ColorSpace) << std::endl - << "Bits Per Color : "sv << desc1.BitsPerColor << std::endl - << "Red Primary : ["sv << desc1.RedPrimary[0] << ',' << desc1.RedPrimary[1] << ']' << std::endl - << "Green Primary : ["sv << desc1.GreenPrimary[0] << ',' << desc1.GreenPrimary[1] << ']' << std::endl - << "Blue Primary : ["sv << desc1.BluePrimary[0] << ',' << desc1.BluePrimary[1] << ']' << std::endl - << "White Point : ["sv << desc1.WhitePoint[0] << ',' << desc1.WhitePoint[1] << ']' << std::endl - << "Min Luminance : "sv << desc1.MinLuminance << " nits"sv << std::endl - << "Max Luminance : "sv << desc1.MaxLuminance << " nits"sv << std::endl - << "Max Full Luminance : "sv << desc1.MaxFullFrameLuminance << " nits"sv; + return desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; } - // Capture format will be determined from the first call to AcquireNextFrame() - capture_format = DXGI_FORMAT_UNKNOWN; + bool + display_base_t::get_hdr_metadata(SS_HDR_METADATA &metadata) { + dxgi::output6_t output6 {}; - return 0; -} + std::memset(&metadata, 0, sizeof(metadata)); -bool display_base_t::is_hdr() { - dxgi::output6_t output6 {}; + auto status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6); + if (FAILED(status)) { + BOOST_LOG(warning) << "Failed to query IDXGIOutput6 from the output"sv; + return false; + } - auto status = output->QueryInterface(IID_IDXGIOutput6, (void **)&output6); - if(FAILED(status)) { - BOOST_LOG(warning) << "Failed to query IDXGIOutput6 from the output"sv; - return false; + DXGI_OUTPUT_DESC1 desc1; + output6->GetDesc1(&desc1); + + // The primaries reported here seem to correspond to scRGB (Rec. 709) + // which we then convert to Rec 2020 in our scRGB FP16 -> PQ shader + // prior to encoding. It's not clear to me if we're supposed to report + // the primaries of the original colorspace or the one we've converted + // it to, but let's just report Rec 2020 primaries and D65 white level + // to avoid confusing clients by reporting Rec 709 primaries with a + // Rec 2020 colorspace. It seems like most clients ignore the primaries + // in the metadata anyway (luminance range is most important). + desc1.RedPrimary[0] = 0.708f; + desc1.RedPrimary[1] = 0.292f; + desc1.GreenPrimary[0] = 0.170f; + desc1.GreenPrimary[1] = 0.797f; + desc1.BluePrimary[0] = 0.131f; + desc1.BluePrimary[1] = 0.046f; + desc1.WhitePoint[0] = 0.3127f; + desc1.WhitePoint[1] = 0.3290f; + + metadata.displayPrimaries[0].x = desc1.RedPrimary[0] * 50000; + metadata.displayPrimaries[0].y = desc1.RedPrimary[1] * 50000; + metadata.displayPrimaries[1].x = desc1.GreenPrimary[0] * 50000; + metadata.displayPrimaries[1].y = desc1.GreenPrimary[1] * 50000; + metadata.displayPrimaries[2].x = desc1.BluePrimary[0] * 50000; + metadata.displayPrimaries[2].y = desc1.BluePrimary[1] * 50000; + + metadata.whitePoint.x = desc1.WhitePoint[0] * 50000; + metadata.whitePoint.y = desc1.WhitePoint[1] * 50000; + + metadata.maxDisplayLuminance = desc1.MaxLuminance; + metadata.minDisplayLuminance = desc1.MinLuminance * 10000; + + // These are content-specific metadata parameters that this interface doesn't give us + metadata.maxContentLightLevel = 0; + metadata.maxFrameAverageLightLevel = 0; + + metadata.maxFullFrameLuminance = desc1.MaxFullFrameLuminance; + + return true; } - DXGI_OUTPUT_DESC1 desc1; - output6->GetDesc1(&desc1); + const char *format_str[] = { + "DXGI_FORMAT_UNKNOWN", + "DXGI_FORMAT_R32G32B32A32_TYPELESS", + "DXGI_FORMAT_R32G32B32A32_FLOAT", + "DXGI_FORMAT_R32G32B32A32_UINT", + "DXGI_FORMAT_R32G32B32A32_SINT", + "DXGI_FORMAT_R32G32B32_TYPELESS", + "DXGI_FORMAT_R32G32B32_FLOAT", + "DXGI_FORMAT_R32G32B32_UINT", + "DXGI_FORMAT_R32G32B32_SINT", + "DXGI_FORMAT_R16G16B16A16_TYPELESS", + "DXGI_FORMAT_R16G16B16A16_FLOAT", + "DXGI_FORMAT_R16G16B16A16_UNORM", + "DXGI_FORMAT_R16G16B16A16_UINT", + "DXGI_FORMAT_R16G16B16A16_SNORM", + "DXGI_FORMAT_R16G16B16A16_SINT", + "DXGI_FORMAT_R32G32_TYPELESS", + "DXGI_FORMAT_R32G32_FLOAT", + "DXGI_FORMAT_R32G32_UINT", + "DXGI_FORMAT_R32G32_SINT", + "DXGI_FORMAT_R32G8X24_TYPELESS", + "DXGI_FORMAT_D32_FLOAT_S8X24_UINT", + "DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS", + "DXGI_FORMAT_X32_TYPELESS_G8X24_UINT", + "DXGI_FORMAT_R10G10B10A2_TYPELESS", + "DXGI_FORMAT_R10G10B10A2_UNORM", + "DXGI_FORMAT_R10G10B10A2_UINT", + "DXGI_FORMAT_R11G11B10_FLOAT", + "DXGI_FORMAT_R8G8B8A8_TYPELESS", + "DXGI_FORMAT_R8G8B8A8_UNORM", + "DXGI_FORMAT_R8G8B8A8_UNORM_SRGB", + "DXGI_FORMAT_R8G8B8A8_UINT", + "DXGI_FORMAT_R8G8B8A8_SNORM", + "DXGI_FORMAT_R8G8B8A8_SINT", + "DXGI_FORMAT_R16G16_TYPELESS", + "DXGI_FORMAT_R16G16_FLOAT", + "DXGI_FORMAT_R16G16_UNORM", + "DXGI_FORMAT_R16G16_UINT", + "DXGI_FORMAT_R16G16_SNORM", + "DXGI_FORMAT_R16G16_SINT", + "DXGI_FORMAT_R32_TYPELESS", + "DXGI_FORMAT_D32_FLOAT", + "DXGI_FORMAT_R32_FLOAT", + "DXGI_FORMAT_R32_UINT", + "DXGI_FORMAT_R32_SINT", + "DXGI_FORMAT_R24G8_TYPELESS", + "DXGI_FORMAT_D24_UNORM_S8_UINT", + "DXGI_FORMAT_R24_UNORM_X8_TYPELESS", + "DXGI_FORMAT_X24_TYPELESS_G8_UINT", + "DXGI_FORMAT_R8G8_TYPELESS", + "DXGI_FORMAT_R8G8_UNORM", + "DXGI_FORMAT_R8G8_UINT", + "DXGI_FORMAT_R8G8_SNORM", + "DXGI_FORMAT_R8G8_SINT", + "DXGI_FORMAT_R16_TYPELESS", + "DXGI_FORMAT_R16_FLOAT", + "DXGI_FORMAT_D16_UNORM", + "DXGI_FORMAT_R16_UNORM", + "DXGI_FORMAT_R16_UINT", + "DXGI_FORMAT_R16_SNORM", + "DXGI_FORMAT_R16_SINT", + "DXGI_FORMAT_R8_TYPELESS", + "DXGI_FORMAT_R8_UNORM", + "DXGI_FORMAT_R8_UINT", + "DXGI_FORMAT_R8_SNORM", + "DXGI_FORMAT_R8_SINT", + "DXGI_FORMAT_A8_UNORM", + "DXGI_FORMAT_R1_UNORM", + "DXGI_FORMAT_R9G9B9E5_SHAREDEXP", + "DXGI_FORMAT_R8G8_B8G8_UNORM", + "DXGI_FORMAT_G8R8_G8B8_UNORM", + "DXGI_FORMAT_BC1_TYPELESS", + "DXGI_FORMAT_BC1_UNORM", + "DXGI_FORMAT_BC1_UNORM_SRGB", + "DXGI_FORMAT_BC2_TYPELESS", + "DXGI_FORMAT_BC2_UNORM", + "DXGI_FORMAT_BC2_UNORM_SRGB", + "DXGI_FORMAT_BC3_TYPELESS", + "DXGI_FORMAT_BC3_UNORM", + "DXGI_FORMAT_BC3_UNORM_SRGB", + "DXGI_FORMAT_BC4_TYPELESS", + "DXGI_FORMAT_BC4_UNORM", + "DXGI_FORMAT_BC4_SNORM", + "DXGI_FORMAT_BC5_TYPELESS", + "DXGI_FORMAT_BC5_UNORM", + "DXGI_FORMAT_BC5_SNORM", + "DXGI_FORMAT_B5G6R5_UNORM", + "DXGI_FORMAT_B5G5R5A1_UNORM", + "DXGI_FORMAT_B8G8R8A8_UNORM", + "DXGI_FORMAT_B8G8R8X8_UNORM", + "DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM", + "DXGI_FORMAT_B8G8R8A8_TYPELESS", + "DXGI_FORMAT_B8G8R8A8_UNORM_SRGB", + "DXGI_FORMAT_B8G8R8X8_TYPELESS", + "DXGI_FORMAT_B8G8R8X8_UNORM_SRGB", + "DXGI_FORMAT_BC6H_TYPELESS", + "DXGI_FORMAT_BC6H_UF16", + "DXGI_FORMAT_BC6H_SF16", + "DXGI_FORMAT_BC7_TYPELESS", + "DXGI_FORMAT_BC7_UNORM", + "DXGI_FORMAT_BC7_UNORM_SRGB", + "DXGI_FORMAT_AYUV", + "DXGI_FORMAT_Y410", + "DXGI_FORMAT_Y416", + "DXGI_FORMAT_NV12", + "DXGI_FORMAT_P010", + "DXGI_FORMAT_P016", + "DXGI_FORMAT_420_OPAQUE", + "DXGI_FORMAT_YUY2", + "DXGI_FORMAT_Y210", + "DXGI_FORMAT_Y216", + "DXGI_FORMAT_NV11", + "DXGI_FORMAT_AI44", + "DXGI_FORMAT_IA44", + "DXGI_FORMAT_P8", + "DXGI_FORMAT_A8P8", + "DXGI_FORMAT_B4G4R4A4_UNORM", - return desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; -} + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, -bool display_base_t::get_hdr_metadata(SS_HDR_METADATA &metadata) { - dxgi::output6_t output6 {}; - - std::memset(&metadata, 0, sizeof(metadata)); - - auto status = output->QueryInterface(IID_IDXGIOutput6, (void **)&output6); - if(FAILED(status)) { - BOOST_LOG(warning) << "Failed to query IDXGIOutput6 from the output"sv; - return false; - } - - DXGI_OUTPUT_DESC1 desc1; - output6->GetDesc1(&desc1); - - // The primaries reported here seem to correspond to scRGB (Rec. 709) - // which we then convert to Rec 2020 in our scRGB FP16 -> PQ shader - // prior to encoding. It's not clear to me if we're supposed to report - // the primaries of the original colorspace or the one we've converted - // it to, but let's just report Rec 2020 primaries and D65 white level - // to avoid confusing clients by reporting Rec 709 primaries with a - // Rec 2020 colorspace. It seems like most clients ignore the primaries - // in the metadata anyway (luminance range is most important). - desc1.RedPrimary[0] = 0.708f; - desc1.RedPrimary[1] = 0.292f; - desc1.GreenPrimary[0] = 0.170f; - desc1.GreenPrimary[1] = 0.797f; - desc1.BluePrimary[0] = 0.131f; - desc1.BluePrimary[1] = 0.046f; - desc1.WhitePoint[0] = 0.3127f; - desc1.WhitePoint[1] = 0.3290f; - - metadata.displayPrimaries[0].x = desc1.RedPrimary[0] * 50000; - metadata.displayPrimaries[0].y = desc1.RedPrimary[1] * 50000; - metadata.displayPrimaries[1].x = desc1.GreenPrimary[0] * 50000; - metadata.displayPrimaries[1].y = desc1.GreenPrimary[1] * 50000; - metadata.displayPrimaries[2].x = desc1.BluePrimary[0] * 50000; - metadata.displayPrimaries[2].y = desc1.BluePrimary[1] * 50000; - - metadata.whitePoint.x = desc1.WhitePoint[0] * 50000; - metadata.whitePoint.y = desc1.WhitePoint[1] * 50000; - - metadata.maxDisplayLuminance = desc1.MaxLuminance; - metadata.minDisplayLuminance = desc1.MinLuminance * 10000; - - // These are content-specific metadata parameters that this interface doesn't give us - metadata.maxContentLightLevel = 0; - metadata.maxFrameAverageLightLevel = 0; - - metadata.maxFullFrameLuminance = desc1.MaxFullFrameLuminance; - - return true; -} - -const char *format_str[] = { - "DXGI_FORMAT_UNKNOWN", - "DXGI_FORMAT_R32G32B32A32_TYPELESS", - "DXGI_FORMAT_R32G32B32A32_FLOAT", - "DXGI_FORMAT_R32G32B32A32_UINT", - "DXGI_FORMAT_R32G32B32A32_SINT", - "DXGI_FORMAT_R32G32B32_TYPELESS", - "DXGI_FORMAT_R32G32B32_FLOAT", - "DXGI_FORMAT_R32G32B32_UINT", - "DXGI_FORMAT_R32G32B32_SINT", - "DXGI_FORMAT_R16G16B16A16_TYPELESS", - "DXGI_FORMAT_R16G16B16A16_FLOAT", - "DXGI_FORMAT_R16G16B16A16_UNORM", - "DXGI_FORMAT_R16G16B16A16_UINT", - "DXGI_FORMAT_R16G16B16A16_SNORM", - "DXGI_FORMAT_R16G16B16A16_SINT", - "DXGI_FORMAT_R32G32_TYPELESS", - "DXGI_FORMAT_R32G32_FLOAT", - "DXGI_FORMAT_R32G32_UINT", - "DXGI_FORMAT_R32G32_SINT", - "DXGI_FORMAT_R32G8X24_TYPELESS", - "DXGI_FORMAT_D32_FLOAT_S8X24_UINT", - "DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS", - "DXGI_FORMAT_X32_TYPELESS_G8X24_UINT", - "DXGI_FORMAT_R10G10B10A2_TYPELESS", - "DXGI_FORMAT_R10G10B10A2_UNORM", - "DXGI_FORMAT_R10G10B10A2_UINT", - "DXGI_FORMAT_R11G11B10_FLOAT", - "DXGI_FORMAT_R8G8B8A8_TYPELESS", - "DXGI_FORMAT_R8G8B8A8_UNORM", - "DXGI_FORMAT_R8G8B8A8_UNORM_SRGB", - "DXGI_FORMAT_R8G8B8A8_UINT", - "DXGI_FORMAT_R8G8B8A8_SNORM", - "DXGI_FORMAT_R8G8B8A8_SINT", - "DXGI_FORMAT_R16G16_TYPELESS", - "DXGI_FORMAT_R16G16_FLOAT", - "DXGI_FORMAT_R16G16_UNORM", - "DXGI_FORMAT_R16G16_UINT", - "DXGI_FORMAT_R16G16_SNORM", - "DXGI_FORMAT_R16G16_SINT", - "DXGI_FORMAT_R32_TYPELESS", - "DXGI_FORMAT_D32_FLOAT", - "DXGI_FORMAT_R32_FLOAT", - "DXGI_FORMAT_R32_UINT", - "DXGI_FORMAT_R32_SINT", - "DXGI_FORMAT_R24G8_TYPELESS", - "DXGI_FORMAT_D24_UNORM_S8_UINT", - "DXGI_FORMAT_R24_UNORM_X8_TYPELESS", - "DXGI_FORMAT_X24_TYPELESS_G8_UINT", - "DXGI_FORMAT_R8G8_TYPELESS", - "DXGI_FORMAT_R8G8_UNORM", - "DXGI_FORMAT_R8G8_UINT", - "DXGI_FORMAT_R8G8_SNORM", - "DXGI_FORMAT_R8G8_SINT", - "DXGI_FORMAT_R16_TYPELESS", - "DXGI_FORMAT_R16_FLOAT", - "DXGI_FORMAT_D16_UNORM", - "DXGI_FORMAT_R16_UNORM", - "DXGI_FORMAT_R16_UINT", - "DXGI_FORMAT_R16_SNORM", - "DXGI_FORMAT_R16_SINT", - "DXGI_FORMAT_R8_TYPELESS", - "DXGI_FORMAT_R8_UNORM", - "DXGI_FORMAT_R8_UINT", - "DXGI_FORMAT_R8_SNORM", - "DXGI_FORMAT_R8_SINT", - "DXGI_FORMAT_A8_UNORM", - "DXGI_FORMAT_R1_UNORM", - "DXGI_FORMAT_R9G9B9E5_SHAREDEXP", - "DXGI_FORMAT_R8G8_B8G8_UNORM", - "DXGI_FORMAT_G8R8_G8B8_UNORM", - "DXGI_FORMAT_BC1_TYPELESS", - "DXGI_FORMAT_BC1_UNORM", - "DXGI_FORMAT_BC1_UNORM_SRGB", - "DXGI_FORMAT_BC2_TYPELESS", - "DXGI_FORMAT_BC2_UNORM", - "DXGI_FORMAT_BC2_UNORM_SRGB", - "DXGI_FORMAT_BC3_TYPELESS", - "DXGI_FORMAT_BC3_UNORM", - "DXGI_FORMAT_BC3_UNORM_SRGB", - "DXGI_FORMAT_BC4_TYPELESS", - "DXGI_FORMAT_BC4_UNORM", - "DXGI_FORMAT_BC4_SNORM", - "DXGI_FORMAT_BC5_TYPELESS", - "DXGI_FORMAT_BC5_UNORM", - "DXGI_FORMAT_BC5_SNORM", - "DXGI_FORMAT_B5G6R5_UNORM", - "DXGI_FORMAT_B5G5R5A1_UNORM", - "DXGI_FORMAT_B8G8R8A8_UNORM", - "DXGI_FORMAT_B8G8R8X8_UNORM", - "DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM", - "DXGI_FORMAT_B8G8R8A8_TYPELESS", - "DXGI_FORMAT_B8G8R8A8_UNORM_SRGB", - "DXGI_FORMAT_B8G8R8X8_TYPELESS", - "DXGI_FORMAT_B8G8R8X8_UNORM_SRGB", - "DXGI_FORMAT_BC6H_TYPELESS", - "DXGI_FORMAT_BC6H_UF16", - "DXGI_FORMAT_BC6H_SF16", - "DXGI_FORMAT_BC7_TYPELESS", - "DXGI_FORMAT_BC7_UNORM", - "DXGI_FORMAT_BC7_UNORM_SRGB", - "DXGI_FORMAT_AYUV", - "DXGI_FORMAT_Y410", - "DXGI_FORMAT_Y416", - "DXGI_FORMAT_NV12", - "DXGI_FORMAT_P010", - "DXGI_FORMAT_P016", - "DXGI_FORMAT_420_OPAQUE", - "DXGI_FORMAT_YUY2", - "DXGI_FORMAT_Y210", - "DXGI_FORMAT_Y216", - "DXGI_FORMAT_NV11", - "DXGI_FORMAT_AI44", - "DXGI_FORMAT_IA44", - "DXGI_FORMAT_P8", - "DXGI_FORMAT_A8P8", - "DXGI_FORMAT_B4G4R4A4_UNORM", - - NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - - "DXGI_FORMAT_P208", - "DXGI_FORMAT_V208", - "DXGI_FORMAT_V408" -}; - -const char *display_base_t::dxgi_format_to_string(DXGI_FORMAT format) { - return format_str[format]; -} - -const char *display_base_t::colorspace_to_string(DXGI_COLOR_SPACE_TYPE type) { - const char *type_str[] = { - "DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709", - "DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709", - "DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P709", - "DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P2020", - "DXGI_COLOR_SPACE_RESERVED", - "DXGI_COLOR_SPACE_YCBCR_FULL_G22_NONE_P709_X601", - "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P601", - "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P601", - "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709", - "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709", - "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P2020", - "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P2020", - "DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020", - "DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020", - "DXGI_COLOR_SPACE_RGB_STUDIO_G2084_NONE_P2020", - "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_TOPLEFT_P2020", - "DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_TOPLEFT_P2020", - "DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020", - "DXGI_COLOR_SPACE_YCBCR_STUDIO_GHLG_TOPLEFT_P2020", - "DXGI_COLOR_SPACE_YCBCR_FULL_GHLG_TOPLEFT_P2020", - "DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P709", - "DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P2020", - "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P709", - "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P2020", - "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_TOPLEFT_P2020", + "DXGI_FORMAT_P208", + "DXGI_FORMAT_V208", + "DXGI_FORMAT_V408" }; - if(type < ARRAYSIZE(type_str)) { - return type_str[type]; + const char * + display_base_t::dxgi_format_to_string(DXGI_FORMAT format) { + return format_str[format]; } - else { - return "UNKNOWN"; - } -} -} // namespace platf::dxgi + const char * + display_base_t::colorspace_to_string(DXGI_COLOR_SPACE_TYPE type) { + const char *type_str[] = { + "DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709", + "DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709", + "DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P709", + "DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P2020", + "DXGI_COLOR_SPACE_RESERVED", + "DXGI_COLOR_SPACE_YCBCR_FULL_G22_NONE_P709_X601", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P601", + "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P601", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709", + "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P2020", + "DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P2020", + "DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020", + "DXGI_COLOR_SPACE_RGB_STUDIO_G2084_NONE_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_TOPLEFT_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_TOPLEFT_P2020", + "DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_GHLG_TOPLEFT_P2020", + "DXGI_COLOR_SPACE_YCBCR_FULL_GHLG_TOPLEFT_P2020", + "DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P709", + "DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P709", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P2020", + "DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_TOPLEFT_P2020", + }; + + if (type < ARRAYSIZE(type_str)) { + return type_str[type]; + } + else { + return "UNKNOWN"; + } + } + +} // namespace platf::dxgi namespace platf { -std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { - if(hwdevice_type == mem_type_e::dxgi) { - auto disp = std::make_shared(); + std::shared_ptr + display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { + if (hwdevice_type == mem_type_e::dxgi) { + auto disp = std::make_shared(); - if(!disp->init(config, display_name)) { - return disp; - } - } - else if(hwdevice_type == mem_type_e::system) { - auto disp = std::make_shared(); - - if(!disp->init(config, display_name)) { - return disp; - } - } - - return nullptr; -} - -std::vector display_names(mem_type_e) { - std::vector display_names; - - HRESULT status; - - BOOST_LOG(debug) << "Detecting monitors..."sv; - - std::wstring_convert, wchar_t> converter; - - // We must set the GPU preference before calling any DXGI APIs! - if(!dxgi::probe_for_gpu_preference(config::video.output_name)) { - BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; - } - - dxgi::factory1_t factory; - status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **)&factory); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']'; - return {}; - } - - dxgi::adapter_t adapter; - for(int x = 0; factory->EnumAdapters1(x, &adapter) != DXGI_ERROR_NOT_FOUND; ++x) { - DXGI_ADAPTER_DESC1 adapter_desc; - adapter->GetDesc1(&adapter_desc); - - BOOST_LOG(debug) - << std::endl - << "====== ADAPTER ====="sv << std::endl - << "Device Name : "sv << converter.to_bytes(adapter_desc.Description) << std::endl - << "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl - << "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl - << "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl - << "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl - << "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl - << std::endl - << " ====== OUTPUT ======"sv << std::endl; - - dxgi::output_t::pointer output_p {}; - for(int y = 0; adapter->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) { - dxgi::output_t output { output_p }; - - DXGI_OUTPUT_DESC desc; - output->GetDesc(&desc); - - auto device_name = converter.to_bytes(desc.DeviceName); - - auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left; - auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top; - - BOOST_LOG(debug) - << " Output Name : "sv << device_name << std::endl - << " AttachedToDesktop : "sv << (desc.AttachedToDesktop ? "yes"sv : "no"sv) << std::endl - << " Resolution : "sv << width << 'x' << height << std::endl - << std::endl; - - // Don't include the display in the list if we can't actually capture it - if(desc.AttachedToDesktop && dxgi::test_dxgi_duplication(adapter, output)) { - display_names.emplace_back(std::move(device_name)); + if (!disp->init(config, display_name)) { + return disp; } } + else if (hwdevice_type == mem_type_e::system) { + auto disp = std::make_shared(); + + if (!disp->init(config, display_name)) { + return disp; + } + } + + return nullptr; } - return display_names; -} + std::vector + display_names(mem_type_e) { + std::vector display_names; -} // namespace platf + HRESULT status; + + BOOST_LOG(debug) << "Detecting monitors..."sv; + + std::wstring_convert, wchar_t> converter; + + // We must set the GPU preference before calling any DXGI APIs! + if (!dxgi::probe_for_gpu_preference(config::video.output_name)) { + BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv; + } + + dxgi::factory1_t factory; + status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']'; + return {}; + } + + dxgi::adapter_t adapter; + for (int x = 0; factory->EnumAdapters1(x, &adapter) != DXGI_ERROR_NOT_FOUND; ++x) { + DXGI_ADAPTER_DESC1 adapter_desc; + adapter->GetDesc1(&adapter_desc); + + BOOST_LOG(debug) + << std::endl + << "====== ADAPTER ====="sv << std::endl + << "Device Name : "sv << converter.to_bytes(adapter_desc.Description) << std::endl + << "Device Vendor ID : 0x"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl + << "Device Device ID : 0x"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl + << "Device Video Mem : "sv << adapter_desc.DedicatedVideoMemory / 1048576 << " MiB"sv << std::endl + << "Device Sys Mem : "sv << adapter_desc.DedicatedSystemMemory / 1048576 << " MiB"sv << std::endl + << "Share Sys Mem : "sv << adapter_desc.SharedSystemMemory / 1048576 << " MiB"sv << std::endl + << std::endl + << " ====== OUTPUT ======"sv << std::endl; + + dxgi::output_t::pointer output_p {}; + for (int y = 0; adapter->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) { + dxgi::output_t output { output_p }; + + DXGI_OUTPUT_DESC desc; + output->GetDesc(&desc); + + auto device_name = converter.to_bytes(desc.DeviceName); + + auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left; + auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top; + + BOOST_LOG(debug) + << " Output Name : "sv << device_name << std::endl + << " AttachedToDesktop : "sv << (desc.AttachedToDesktop ? "yes"sv : "no"sv) << std::endl + << " Resolution : "sv << width << 'x' << height << std::endl + << std::endl; + + // Don't include the display in the list if we can't actually capture it + if (desc.AttachedToDesktop && dxgi::test_dxgi_duplication(adapter, output)) { + display_names.emplace_back(std::move(device_name)); + } + } + } + + return display_names; + } + +} // namespace platf diff --git a/src/platform/windows/display_ram.cpp b/src/platform/windows/display_ram.cpp index c3f64bdb..581d925c 100644 --- a/src/platform/windows/display_ram.cpp +++ b/src/platform/windows/display_ram.cpp @@ -2,366 +2,378 @@ #include "src/main.h" namespace platf { -using namespace std::literals; + using namespace std::literals; } namespace platf::dxgi { -struct img_t : public ::platf::img_t { - ~img_t() override { - delete[] data; - data = nullptr; - } -}; + struct img_t: public ::platf::img_t { + ~img_t() override { + delete[] data; + data = nullptr; + } + }; -void blend_cursor_monochrome(const cursor_t &cursor, img_t &img) { - int height = cursor.shape_info.Height / 2; - int width = cursor.shape_info.Width; - int pitch = cursor.shape_info.Pitch; + void + blend_cursor_monochrome(const cursor_t &cursor, img_t &img) { + int height = cursor.shape_info.Height / 2; + int width = cursor.shape_info.Width; + int pitch = cursor.shape_info.Pitch; - // img cursor.{x,y} < 0, skip parts of the cursor.img_data - auto cursor_skip_y = -std::min(0, cursor.y); - auto cursor_skip_x = -std::min(0, cursor.x); + // img cursor.{x,y} < 0, skip parts of the cursor.img_data + auto cursor_skip_y = -std::min(0, cursor.y); + auto cursor_skip_x = -std::min(0, cursor.x); - // img cursor.{x,y} > img.{x,y}, truncate parts of the cursor.img_data - auto cursor_truncate_y = std::max(0, cursor.y - img.height); - auto cursor_truncate_x = std::max(0, cursor.x - img.width); + // img cursor.{x,y} > img.{x,y}, truncate parts of the cursor.img_data + auto cursor_truncate_y = std::max(0, cursor.y - img.height); + auto cursor_truncate_x = std::max(0, cursor.x - img.width); - auto cursor_width = width - cursor_skip_x - cursor_truncate_x; - auto cursor_height = height - cursor_skip_y - cursor_truncate_y; + auto cursor_width = width - cursor_skip_x - cursor_truncate_x; + auto cursor_height = height - cursor_skip_y - cursor_truncate_y; - if(cursor_height > height || cursor_width > width) { - return; - } + if (cursor_height > height || cursor_width > width) { + return; + } - auto img_skip_y = std::max(0, cursor.y); - auto img_skip_x = std::max(0, cursor.x); + auto img_skip_y = std::max(0, cursor.y); + auto img_skip_x = std::max(0, cursor.x); - auto cursor_img_data = cursor.img_data.data() + cursor_skip_y * pitch; + auto cursor_img_data = cursor.img_data.data() + cursor_skip_y * pitch; - int delta_height = std::min(cursor_height - cursor_truncate_y, std::max(0, img.height - img_skip_y)); - int delta_width = std::min(cursor_width - cursor_truncate_x, std::max(0, img.width - img_skip_x)); + int delta_height = std::min(cursor_height - cursor_truncate_y, std::max(0, img.height - img_skip_y)); + int delta_width = std::min(cursor_width - cursor_truncate_x, std::max(0, img.width - img_skip_x)); - auto pixels_per_byte = width / pitch; - auto bytes_per_row = delta_width / pixels_per_byte; + auto pixels_per_byte = width / pitch; + auto bytes_per_row = delta_width / pixels_per_byte; - auto img_data = (int *)img.data; - for(int i = 0; i < delta_height; ++i) { - auto and_mask = &cursor_img_data[i * pitch]; - auto xor_mask = &cursor_img_data[(i + height) * pitch]; + auto img_data = (int *) img.data; + for (int i = 0; i < delta_height; ++i) { + auto and_mask = &cursor_img_data[i * pitch]; + auto xor_mask = &cursor_img_data[(i + height) * pitch]; - auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x]; + auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x]; - auto skip_x = cursor_skip_x; - for(int x = 0; x < bytes_per_row; ++x) { - for(auto bit = 0u; bit < 8; ++bit) { - if(skip_x > 0) { - --skip_x; + auto skip_x = cursor_skip_x; + for (int x = 0; x < bytes_per_row; ++x) { + for (auto bit = 0u; bit < 8; ++bit) { + if (skip_x > 0) { + --skip_x; - continue; + continue; + } + + int and_ = *and_mask & (1 << (7 - bit)) ? -1 : 0; + int xor_ = *xor_mask & (1 << (7 - bit)) ? -1 : 0; + + *img_pixel_p &= and_; + *img_pixel_p ^= xor_; + + ++img_pixel_p; } - int and_ = *and_mask & (1 << (7 - bit)) ? -1 : 0; - int xor_ = *xor_mask & (1 << (7 - bit)) ? -1 : 0; + ++and_mask; + ++xor_mask; + } + } + } - *img_pixel_p &= and_; - *img_pixel_p ^= xor_; + void + apply_color_alpha(int *img_pixel_p, int cursor_pixel) { + auto colors_out = (std::uint8_t *) &cursor_pixel; + auto colors_in = (std::uint8_t *) img_pixel_p; + //TODO: When use of IDXGIOutput5 is implemented, support different color formats + auto alpha = colors_out[3]; + if (alpha == 255) { + *img_pixel_p = cursor_pixel; + } + else { + colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255; + colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255; + colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255; + } + } + + void + apply_color_masked(int *img_pixel_p, int cursor_pixel) { + //TODO: When use of IDXGIOutput5 is implemented, support different color formats + auto alpha = ((std::uint8_t *) &cursor_pixel)[3]; + if (alpha == 0xFF) { + *img_pixel_p ^= cursor_pixel; + } + else { + *img_pixel_p = cursor_pixel; + } + } + + void + blend_cursor_color(const cursor_t &cursor, img_t &img, const bool masked) { + int height = cursor.shape_info.Height; + int width = cursor.shape_info.Width; + int pitch = cursor.shape_info.Pitch; + + // img cursor.y < 0, skip parts of the cursor.img_data + auto cursor_skip_y = -std::min(0, cursor.y); + auto cursor_skip_x = -std::min(0, cursor.x); + + // img cursor.{x,y} > img.{x,y}, truncate parts of the cursor.img_data + auto cursor_truncate_y = std::max(0, cursor.y - img.height); + auto cursor_truncate_x = std::max(0, cursor.x - img.width); + + auto img_skip_y = std::max(0, cursor.y); + auto img_skip_x = std::max(0, cursor.x); + + auto cursor_width = width - cursor_skip_x - cursor_truncate_x; + auto cursor_height = height - cursor_skip_y - cursor_truncate_y; + + if (cursor_height > height || cursor_width > width) { + return; + } + + auto cursor_img_data = (int *) &cursor.img_data[cursor_skip_y * pitch]; + + int delta_height = std::min(cursor_height - cursor_truncate_y, std::max(0, img.height - img_skip_y)); + int delta_width = std::min(cursor_width - cursor_truncate_x, std::max(0, img.width - img_skip_x)); + + auto img_data = (int *) img.data; + + for (int i = 0; i < delta_height; ++i) { + auto cursor_begin = &cursor_img_data[i * cursor.shape_info.Width + cursor_skip_x]; + auto cursor_end = &cursor_begin[delta_width]; + + auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x]; + std::for_each(cursor_begin, cursor_end, [&](int cursor_pixel) { + if (masked) { + apply_color_masked(img_pixel_p, cursor_pixel); + } + else { + apply_color_alpha(img_pixel_p, cursor_pixel); + } ++img_pixel_p; - } - - ++and_mask; - ++xor_mask; - } - } -} - -void apply_color_alpha(int *img_pixel_p, int cursor_pixel) { - auto colors_out = (std::uint8_t *)&cursor_pixel; - auto colors_in = (std::uint8_t *)img_pixel_p; - - //TODO: When use of IDXGIOutput5 is implemented, support different color formats - auto alpha = colors_out[3]; - if(alpha == 255) { - *img_pixel_p = cursor_pixel; - } - else { - colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255; - colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255; - colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255; - } -} - -void apply_color_masked(int *img_pixel_p, int cursor_pixel) { - //TODO: When use of IDXGIOutput5 is implemented, support different color formats - auto alpha = ((std::uint8_t *)&cursor_pixel)[3]; - if(alpha == 0xFF) { - *img_pixel_p ^= cursor_pixel; - } - else { - *img_pixel_p = cursor_pixel; - } -} - -void blend_cursor_color(const cursor_t &cursor, img_t &img, const bool masked) { - int height = cursor.shape_info.Height; - int width = cursor.shape_info.Width; - int pitch = cursor.shape_info.Pitch; - - // img cursor.y < 0, skip parts of the cursor.img_data - auto cursor_skip_y = -std::min(0, cursor.y); - auto cursor_skip_x = -std::min(0, cursor.x); - - // img cursor.{x,y} > img.{x,y}, truncate parts of the cursor.img_data - auto cursor_truncate_y = std::max(0, cursor.y - img.height); - auto cursor_truncate_x = std::max(0, cursor.x - img.width); - - auto img_skip_y = std::max(0, cursor.y); - auto img_skip_x = std::max(0, cursor.x); - - auto cursor_width = width - cursor_skip_x - cursor_truncate_x; - auto cursor_height = height - cursor_skip_y - cursor_truncate_y; - - if(cursor_height > height || cursor_width > width) { - return; - } - - auto cursor_img_data = (int *)&cursor.img_data[cursor_skip_y * pitch]; - - int delta_height = std::min(cursor_height - cursor_truncate_y, std::max(0, img.height - img_skip_y)); - int delta_width = std::min(cursor_width - cursor_truncate_x, std::max(0, img.width - img_skip_x)); - - auto img_data = (int *)img.data; - - for(int i = 0; i < delta_height; ++i) { - auto cursor_begin = &cursor_img_data[i * cursor.shape_info.Width + cursor_skip_x]; - auto cursor_end = &cursor_begin[delta_width]; - - auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x]; - std::for_each(cursor_begin, cursor_end, [&](int cursor_pixel) { - if(masked) { - apply_color_masked(img_pixel_p, cursor_pixel); - } - else { - apply_color_alpha(img_pixel_p, cursor_pixel); - } - ++img_pixel_p; - }); - } -} - -void blend_cursor(const cursor_t &cursor, img_t &img) { - switch(cursor.shape_info.Type) { - case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR: - blend_cursor_color(cursor, img, false); - break; - case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME: - blend_cursor_monochrome(cursor, img); - break; - case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: - blend_cursor_color(cursor, img, true); - break; - default: - BOOST_LOG(warning) << "Unsupported cursor format ["sv << cursor.shape_info.Type << ']'; - } -} - -capture_e display_ram_t::snapshot(::platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) { - auto img = (img_t *)img_base; - - HRESULT status; - - DXGI_OUTDUPL_FRAME_INFO frame_info; - - resource_t::pointer res_p {}; - auto capture_status = dup.next_frame(frame_info, timeout, &res_p); - resource_t res { res_p }; - - if(capture_status != capture_e::ok) { - return capture_status; - } - - const bool mouse_update_flag = frame_info.LastMouseUpdateTime.QuadPart != 0 || frame_info.PointerShapeBufferSize > 0; - const bool frame_update_flag = frame_info.AccumulatedFrames != 0 || frame_info.LastPresentTime.QuadPart != 0; - const bool update_flag = mouse_update_flag || frame_update_flag; - - if(!update_flag) { - return capture_e::timeout; - } - - if(frame_info.PointerShapeBufferSize > 0) { - auto &img_data = cursor.img_data; - - img_data.resize(frame_info.PointerShapeBufferSize); - - UINT dummy; - status = dup.dup->GetFramePointerShape(img_data.size(), img_data.data(), &dummy, &cursor.shape_info); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to get new pointer shape [0x"sv << util::hex(status).to_string_view() << ']'; - - return capture_e::error; + }); } } - if(frame_info.LastMouseUpdateTime.QuadPart) { - cursor.x = frame_info.PointerPosition.Position.x; - cursor.y = frame_info.PointerPosition.Position.y; - cursor.visible = frame_info.PointerPosition.Visible; + void + blend_cursor(const cursor_t &cursor, img_t &img) { + switch (cursor.shape_info.Type) { + case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR: + blend_cursor_color(cursor, img, false); + break; + case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME: + blend_cursor_monochrome(cursor, img); + break; + case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: + blend_cursor_color(cursor, img, true); + break; + default: + BOOST_LOG(warning) << "Unsupported cursor format ["sv << cursor.shape_info.Type << ']'; + } } - if(frame_update_flag) { - { - texture2d_t src {}; - status = res->QueryInterface(IID_ID3D11Texture2D, (void **)&src); + capture_e + display_ram_t::snapshot(::platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) { + auto img = (img_t *) img_base; + + HRESULT status; + + DXGI_OUTDUPL_FRAME_INFO frame_info; + + resource_t::pointer res_p {}; + auto capture_status = dup.next_frame(frame_info, timeout, &res_p); + resource_t res { res_p }; + + if (capture_status != capture_e::ok) { + return capture_status; + } + + const bool mouse_update_flag = frame_info.LastMouseUpdateTime.QuadPart != 0 || frame_info.PointerShapeBufferSize > 0; + const bool frame_update_flag = frame_info.AccumulatedFrames != 0 || frame_info.LastPresentTime.QuadPart != 0; + const bool update_flag = mouse_update_flag || frame_update_flag; + + if (!update_flag) { + return capture_e::timeout; + } + + if (frame_info.PointerShapeBufferSize > 0) { + auto &img_data = cursor.img_data; + + img_data.resize(frame_info.PointerShapeBufferSize); + + UINT dummy; + status = dup.dup->GetFramePointerShape(img_data.size(), img_data.data(), &dummy, &cursor.shape_info); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to get new pointer shape [0x"sv << util::hex(status).to_string_view() << ']'; + + return capture_e::error; + } + } + + if (frame_info.LastMouseUpdateTime.QuadPart) { + cursor.x = frame_info.PointerPosition.Position.x; + cursor.y = frame_info.PointerPosition.Position.y; + cursor.visible = frame_info.PointerPosition.Visible; + } + + if (frame_update_flag) { + { + texture2d_t src {}; + status = res->QueryInterface(IID_ID3D11Texture2D, (void **) &src); + + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't query interface [0x"sv << util::hex(status).to_string_view() << ']'; + return capture_e::error; + } + + D3D11_TEXTURE2D_DESC desc; + src->GetDesc(&desc); + + // If we don't know the capture format yet, grab it from this texture and create the staging texture + if (capture_format == DXGI_FORMAT_UNKNOWN) { + capture_format = desc.Format; + BOOST_LOG(info) << "Capture format ["sv << dxgi_format_to_string(capture_format) << ']'; + + D3D11_TEXTURE2D_DESC t {}; + t.Width = width; + t.Height = height; + t.MipLevels = 1; + t.ArraySize = 1; + t.SampleDesc.Count = 1; + t.Usage = D3D11_USAGE_STAGING; + t.Format = capture_format; + t.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + + auto status = device->CreateTexture2D(&t, nullptr, &texture); + + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create staging texture [0x"sv << util::hex(status).to_string_view() << ']'; + return capture_e::error; + } + } + + // It's possible for our display enumeration to race with mode changes and result in + // mismatched image pool and desktop texture sizes. If this happens, just reinit again. + if (desc.Width != width || desc.Height != height) { + BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; + return capture_e::reinit; + } + + // It's also possible for the capture format to change on the fly. If that happens, + // reinitialize capture to try format detection again and create new images. + if (capture_format != desc.Format) { + BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; + return capture_e::reinit; + } + + //Copy from GPU to CPU + device_ctx->CopyResource(texture.get(), src.get()); + } + } + + // If we don't know the final capture format yet, encode a dummy image + if (capture_format == DXGI_FORMAT_UNKNOWN) { + BOOST_LOG(debug) << "Capture format is still unknown. Encoding a blank image"sv; + + if (dummy_img(img)) { + return capture_e::error; + } + } + else { + // Map the staging texture for CPU access (making it inaccessible for the GPU) + status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to map texture [0x"sv << util::hex(status).to_string_view() << ']'; - if(FAILED(status)) { - BOOST_LOG(error) << "Couldn't query interface [0x"sv << util::hex(status).to_string_view() << ']'; return capture_e::error; } - D3D11_TEXTURE2D_DESC desc; - src->GetDesc(&desc); - - // If we don't know the capture format yet, grab it from this texture and create the staging texture - if(capture_format == DXGI_FORMAT_UNKNOWN) { - capture_format = desc.Format; - BOOST_LOG(info) << "Capture format ["sv << dxgi_format_to_string(capture_format) << ']'; - - D3D11_TEXTURE2D_DESC t {}; - t.Width = width; - t.Height = height; - t.MipLevels = 1; - t.ArraySize = 1; - t.SampleDesc.Count = 1; - t.Usage = D3D11_USAGE_STAGING; - t.Format = capture_format; - t.CPUAccessFlags = D3D11_CPU_ACCESS_READ; - - auto status = device->CreateTexture2D(&t, nullptr, &texture); - - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create staging texture [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; - } + // Now that we know the capture format, we can finish creating the image + if (complete_img(img, false)) { + device_ctx->Unmap(texture.get(), 0); + img_info.pData = nullptr; + return capture_e::error; } - // It's possible for our display enumeration to race with mode changes and result in - // mismatched image pool and desktop texture sizes. If this happens, just reinit again. - if(desc.Width != width || desc.Height != height) { - BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; - return capture_e::reinit; - } + std::copy_n((std::uint8_t *) img_info.pData, height * img_info.RowPitch, (std::uint8_t *) img->data); - // It's also possible for the capture format to change on the fly. If that happens, - // reinitialize capture to try format detection again and create new images. - if(capture_format != desc.Format) { - BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; - return capture_e::reinit; - } - - //Copy from GPU to CPU - device_ctx->CopyResource(texture.get(), src.get()); - } - } - - // If we don't know the final capture format yet, encode a dummy image - if(capture_format == DXGI_FORMAT_UNKNOWN) { - BOOST_LOG(debug) << "Capture format is still unknown. Encoding a blank image"sv; - - if(dummy_img(img)) { - return capture_e::error; - } - } - else { - // Map the staging texture for CPU access (making it inaccessible for the GPU) - status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to map texture [0x"sv << util::hex(status).to_string_view() << ']'; - - return capture_e::error; - } - - // Now that we know the capture format, we can finish creating the image - if(complete_img(img, false)) { + // Unmap the staging texture to allow GPU access again device_ctx->Unmap(texture.get(), 0); img_info.pData = nullptr; - return capture_e::error; } - std::copy_n((std::uint8_t *)img_info.pData, height * img_info.RowPitch, (std::uint8_t *)img->data); + if (cursor_visible && cursor.visible) { + blend_cursor(cursor, *img); + } - // Unmap the staging texture to allow GPU access again - device_ctx->Unmap(texture.get(), 0); - img_info.pData = nullptr; + return capture_e::ok; } - if(cursor_visible && cursor.visible) { - blend_cursor(cursor, *img); + std::shared_ptr + display_ram_t::alloc_img() { + auto img = std::make_shared(); + + // Initialize fields that are format-independent + img->width = width; + img->height = height; + + return img; } - return capture_e::ok; -} + int + display_ram_t::complete_img(platf::img_t *img, bool dummy) { + // If this is not a dummy image, we must know the format by now + if (!dummy && capture_format == DXGI_FORMAT_UNKNOWN) { + BOOST_LOG(error) << "display_ram_t::complete_img() called with unknown capture format!"; + return -1; + } -std::shared_ptr display_ram_t::alloc_img() { - auto img = std::make_shared(); + img->pixel_pitch = get_pixel_pitch(); - // Initialize fields that are format-independent - img->width = width; - img->height = height; + if (dummy && !img->row_pitch) { + // Assume our dummy image will have no padding + img->row_pitch = img->pixel_pitch * img->width; + } - return img; -} + // Reallocate the image buffer if the pitch changes + if (!dummy && img->row_pitch != img_info.RowPitch) { + img->row_pitch = img_info.RowPitch; + delete img->data; + img->data = nullptr; + } -int display_ram_t::complete_img(platf::img_t *img, bool dummy) { - // If this is not a dummy image, we must know the format by now - if(!dummy && capture_format == DXGI_FORMAT_UNKNOWN) { - BOOST_LOG(error) << "display_ram_t::complete_img() called with unknown capture format!"; - return -1; + if (!img->data) { + img->data = new std::uint8_t[img->row_pitch * height]; + } + + return 0; } - img->pixel_pitch = get_pixel_pitch(); + int + display_ram_t::dummy_img(platf::img_t *img) { + if (complete_img(img, true)) { + return -1; + } - if(dummy && !img->row_pitch) { - // Assume our dummy image will have no padding - img->row_pitch = img->pixel_pitch * img->width; + std::fill_n((std::uint8_t *) img->data, height * img->row_pitch, 0); + return 0; } - // Reallocate the image buffer if the pitch changes - if(!dummy && img->row_pitch != img_info.RowPitch) { - img->row_pitch = img_info.RowPitch; - delete img->data; - img->data = nullptr; + std::vector + display_ram_t::get_supported_sdr_capture_formats() { + return { DXGI_FORMAT_B8G8R8A8_UNORM }; } - if(!img->data) { - img->data = new std::uint8_t[img->row_pitch * height]; + std::vector + display_ram_t::get_supported_hdr_capture_formats() { + // HDR is unsupported + return {}; } - return 0; -} + int + display_ram_t::init(const ::video::config_t &config, const std::string &display_name) { + if (display_base_t::init(config, display_name)) { + return -1; + } -int display_ram_t::dummy_img(platf::img_t *img) { - if(complete_img(img, true)) { - return -1; + return 0; } - - std::fill_n((std::uint8_t *)img->data, height * img->row_pitch, 0); - return 0; -} - -std::vector display_ram_t::get_supported_sdr_capture_formats() { - return { DXGI_FORMAT_B8G8R8A8_UNORM }; -} - -std::vector display_ram_t::get_supported_hdr_capture_formats() { - // HDR is unsupported - return {}; -} - -int display_ram_t::init(const ::video::config_t &config, const std::string &display_name) { - if(display_base_t::init(config, display_name)) { - return -1; - } - - return 0; -} -} // namespace platf::dxgi +} // namespace platf::dxgi diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index ab63974b..b6d51335 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -14,13 +14,13 @@ extern "C" { #include "src/main.h" #include "src/video.h" - #define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx" namespace platf { -using namespace std::literals; + using namespace std::literals; } -static void free_frame(AVFrame *frame) { +static void +free_frame(AVFrame *frame) { av_frame_free(&frame); } @@ -28,602 +28,1021 @@ using frame_t = util::safe_ptr; namespace platf::dxgi { -template -buf_t make_buffer(device_t::pointer device, const T &t) { - static_assert(sizeof(T) % 16 == 0, "Buffer needs to be aligned on a 16-byte alignment"); + template + buf_t + make_buffer(device_t::pointer device, const T &t) { + static_assert(sizeof(T) % 16 == 0, "Buffer needs to be aligned on a 16-byte alignment"); - D3D11_BUFFER_DESC buffer_desc { - sizeof(T), - D3D11_USAGE_IMMUTABLE, - D3D11_BIND_CONSTANT_BUFFER - }; + D3D11_BUFFER_DESC buffer_desc { + sizeof(T), + D3D11_USAGE_IMMUTABLE, + D3D11_BIND_CONSTANT_BUFFER + }; - D3D11_SUBRESOURCE_DATA init_data { - &t - }; + D3D11_SUBRESOURCE_DATA init_data { + &t + }; - buf_t::pointer buf_p; - auto status = device->CreateBuffer(&buffer_desc, &init_data, &buf_p); - if(status) { - BOOST_LOG(error) << "Failed to create buffer: [0x"sv << util::hex(status).to_string_view() << ']'; - return nullptr; - } - - return buf_t { buf_p }; -} - -blend_t make_blend(device_t::pointer device, bool enable, bool invert) { - D3D11_BLEND_DESC bdesc {}; - auto &rt = bdesc.RenderTarget[0]; - rt.BlendEnable = enable; - rt.RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; - - if(enable) { - rt.BlendOp = D3D11_BLEND_OP_ADD; - rt.BlendOpAlpha = D3D11_BLEND_OP_ADD; - - if(invert) { - // Invert colors - rt.SrcBlend = D3D11_BLEND_INV_DEST_COLOR; - rt.DestBlend = D3D11_BLEND_INV_SRC_COLOR; - } - else { - // Regular alpha blending - rt.SrcBlend = D3D11_BLEND_SRC_ALPHA; - rt.DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + buf_t::pointer buf_p; + auto status = device->CreateBuffer(&buffer_desc, &init_data, &buf_p); + if (status) { + BOOST_LOG(error) << "Failed to create buffer: [0x"sv << util::hex(status).to_string_view() << ']'; + return nullptr; } - rt.SrcBlendAlpha = D3D11_BLEND_ZERO; - rt.DestBlendAlpha = D3D11_BLEND_ZERO; + return buf_t { buf_p }; } - blend_t blend; - auto status = device->CreateBlendState(&bdesc, &blend); - if(status) { - BOOST_LOG(error) << "Failed to create blend state: [0x"sv << util::hex(status).to_string_view() << ']'; - return nullptr; - } + blend_t + make_blend(device_t::pointer device, bool enable, bool invert) { + D3D11_BLEND_DESC bdesc {}; + auto &rt = bdesc.RenderTarget[0]; + rt.BlendEnable = enable; + rt.RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; - return blend; -} + if (enable) { + rt.BlendOp = D3D11_BLEND_OP_ADD; + rt.BlendOpAlpha = D3D11_BLEND_OP_ADD; -blob_t convert_UV_vs_hlsl; -blob_t convert_UV_ps_hlsl; -blob_t convert_UV_PQ_ps_hlsl; -blob_t scene_vs_hlsl; -blob_t convert_Y_ps_hlsl; -blob_t convert_Y_PQ_ps_hlsl; -blob_t scene_ps_hlsl; -blob_t scene_NW_ps_hlsl; - -struct img_d3d_t : public platf::img_t { - std::shared_ptr display; - - // These objects are owned by the display_t's ID3D11Device - texture2d_t capture_texture; - render_target_t capture_rt; - keyed_mutex_t capture_mutex; - - // This is the shared handle used by hwdevice_t to open capture_texture - HANDLE encoder_texture_handle = {}; - - // Set to true if the image corresponds to a dummy texture used prior to - // the first successful capture of a desktop frame - bool dummy = false; - - // Unique identifier for this image - uint32_t id = 0; - - virtual ~img_d3d_t() override { - if(encoder_texture_handle) { - CloseHandle(encoder_texture_handle); - } - }; -}; - -util::buffer_t make_cursor_xor_image(const util::buffer_t &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) { - constexpr std::uint32_t inverted = 0xFFFFFFFF; - constexpr std::uint32_t transparent = 0; - - switch(shape_info.Type) { - case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR: - // This type doesn't require any XOR-blending - return {}; - case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: { - util::buffer_t cursor_img = img_data; - std::for_each((std::uint32_t *)std::begin(cursor_img), (std::uint32_t *)std::end(cursor_img), [](auto &pixel) { - auto alpha = (std::uint8_t)((pixel >> 24) & 0xFF); - if(alpha == 0xFF) { - // Pixels with 0xFF alpha will be XOR-blended as is. - } - else if(alpha == 0x00) { - // Pixels with 0x00 alpha will be blended by make_cursor_alpha_image(). - // We make them transparent for the XOR-blended cursor image. - pixel = transparent; + if (invert) { + // Invert colors + rt.SrcBlend = D3D11_BLEND_INV_DEST_COLOR; + rt.DestBlend = D3D11_BLEND_INV_SRC_COLOR; } else { - // Other alpha values are illegal in masked color cursors - BOOST_LOG(warning) << "Illegal alpha value in masked color cursor: " << alpha; + // Regular alpha blending + rt.SrcBlend = D3D11_BLEND_SRC_ALPHA; + rt.DestBlend = D3D11_BLEND_INV_SRC_ALPHA; } - }); + + rt.SrcBlendAlpha = D3D11_BLEND_ZERO; + rt.DestBlendAlpha = D3D11_BLEND_ZERO; + } + + blend_t blend; + auto status = device->CreateBlendState(&bdesc, &blend); + if (status) { + BOOST_LOG(error) << "Failed to create blend state: [0x"sv << util::hex(status).to_string_view() << ']'; + return nullptr; + } + + return blend; + } + + blob_t convert_UV_vs_hlsl; + blob_t convert_UV_ps_hlsl; + blob_t convert_UV_PQ_ps_hlsl; + blob_t scene_vs_hlsl; + blob_t convert_Y_ps_hlsl; + blob_t convert_Y_PQ_ps_hlsl; + blob_t scene_ps_hlsl; + blob_t scene_NW_ps_hlsl; + + struct img_d3d_t: public platf::img_t { + std::shared_ptr display; + + // These objects are owned by the display_t's ID3D11Device + texture2d_t capture_texture; + render_target_t capture_rt; + keyed_mutex_t capture_mutex; + + // This is the shared handle used by hwdevice_t to open capture_texture + HANDLE encoder_texture_handle = {}; + + // Set to true if the image corresponds to a dummy texture used prior to + // the first successful capture of a desktop frame + bool dummy = false; + + // Unique identifier for this image + uint32_t id = 0; + + virtual ~img_d3d_t() override { + if (encoder_texture_handle) { + CloseHandle(encoder_texture_handle); + } + }; + }; + + util::buffer_t + make_cursor_xor_image(const util::buffer_t &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) { + constexpr std::uint32_t inverted = 0xFFFFFFFF; + constexpr std::uint32_t transparent = 0; + + switch (shape_info.Type) { + case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR: + // This type doesn't require any XOR-blending + return {}; + case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: { + util::buffer_t cursor_img = img_data; + std::for_each((std::uint32_t *) std::begin(cursor_img), (std::uint32_t *) std::end(cursor_img), [](auto &pixel) { + auto alpha = (std::uint8_t)((pixel >> 24) & 0xFF); + if (alpha == 0xFF) { + // Pixels with 0xFF alpha will be XOR-blended as is. + } + else if (alpha == 0x00) { + // Pixels with 0x00 alpha will be blended by make_cursor_alpha_image(). + // We make them transparent for the XOR-blended cursor image. + pixel = transparent; + } + else { + // Other alpha values are illegal in masked color cursors + BOOST_LOG(warning) << "Illegal alpha value in masked color cursor: " << alpha; + } + }); + return cursor_img; + } + case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME: + // Monochrome is handled below + break; + default: + BOOST_LOG(error) << "Invalid cursor shape type: " << shape_info.Type; + return {}; + } + + shape_info.Height /= 2; + + util::buffer_t cursor_img { shape_info.Width * shape_info.Height * 4 }; + + auto bytes = shape_info.Pitch * shape_info.Height; + auto pixel_begin = (std::uint32_t *) std::begin(cursor_img); + auto pixel_data = pixel_begin; + auto and_mask = std::begin(img_data); + auto xor_mask = std::begin(img_data) + bytes; + + for (auto x = 0; x < bytes; ++x) { + for (auto c = 7; c >= 0; --c) { + auto bit = 1 << c; + auto color_type = ((*and_mask & bit) ? 1 : 0) + ((*xor_mask & bit) ? 2 : 0); + + switch (color_type) { + case 0: // Opaque black (handled by alpha-blending) + case 2: // Opaque white (handled by alpha-blending) + case 1: // Color of screen (transparent) + *pixel_data = transparent; + break; + case 3: // Inverse of screen + *pixel_data = inverted; + break; + } + + ++pixel_data; + } + ++and_mask; + ++xor_mask; + } + return cursor_img; } - case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME: - // Monochrome is handled below - break; - default: - BOOST_LOG(error) << "Invalid cursor shape type: " << shape_info.Type; - return {}; - } - shape_info.Height /= 2; + util::buffer_t + make_cursor_alpha_image(const util::buffer_t &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) { + constexpr std::uint32_t black = 0xFF000000; + constexpr std::uint32_t white = 0xFFFFFFFF; + constexpr std::uint32_t transparent = 0; - util::buffer_t cursor_img { shape_info.Width * shape_info.Height * 4 }; - - auto bytes = shape_info.Pitch * shape_info.Height; - auto pixel_begin = (std::uint32_t *)std::begin(cursor_img); - auto pixel_data = pixel_begin; - auto and_mask = std::begin(img_data); - auto xor_mask = std::begin(img_data) + bytes; - - for(auto x = 0; x < bytes; ++x) { - for(auto c = 7; c >= 0; --c) { - auto bit = 1 << c; - auto color_type = ((*and_mask & bit) ? 1 : 0) + ((*xor_mask & bit) ? 2 : 0); - - switch(color_type) { - case 0: // Opaque black (handled by alpha-blending) - case 2: // Opaque white (handled by alpha-blending) - case 1: // Color of screen (transparent) - *pixel_data = transparent; - break; - case 3: // Inverse of screen - *pixel_data = inverted; - break; + switch (shape_info.Type) { + case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: { + util::buffer_t cursor_img = img_data; + std::for_each((std::uint32_t *) std::begin(cursor_img), (std::uint32_t *) std::end(cursor_img), [](auto &pixel) { + auto alpha = (std::uint8_t)((pixel >> 24) & 0xFF); + if (alpha == 0xFF) { + // Pixels with 0xFF alpha will be XOR-blended by make_cursor_xor_image(). + // We make them transparent for the alpha-blended cursor image. + pixel = transparent; + } + else if (alpha == 0x00) { + // Pixels with 0x00 alpha will be blended as opaque with the alpha-blended image. + pixel |= 0xFF000000; + } + else { + // Other alpha values are illegal in masked color cursors + BOOST_LOG(warning) << "Illegal alpha value in masked color cursor: " << alpha; + } + }); + return cursor_img; } - - ++pixel_data; + case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR: + // Color cursors are just an ARGB bitmap which requires no processing. + return img_data; + case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME: + // Monochrome cursors are handled below. + break; + default: + BOOST_LOG(error) << "Invalid cursor shape type: " << shape_info.Type; + return {}; } - ++and_mask; - ++xor_mask; - } - return cursor_img; -} + shape_info.Height /= 2; -util::buffer_t make_cursor_alpha_image(const util::buffer_t &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) { - constexpr std::uint32_t black = 0xFF000000; - constexpr std::uint32_t white = 0xFFFFFFFF; - constexpr std::uint32_t transparent = 0; + util::buffer_t cursor_img { shape_info.Width * shape_info.Height * 4 }; - switch(shape_info.Type) { - case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: { - util::buffer_t cursor_img = img_data; - std::for_each((std::uint32_t *)std::begin(cursor_img), (std::uint32_t *)std::end(cursor_img), [](auto &pixel) { - auto alpha = (std::uint8_t)((pixel >> 24) & 0xFF); - if(alpha == 0xFF) { - // Pixels with 0xFF alpha will be XOR-blended by make_cursor_xor_image(). - // We make them transparent for the alpha-blended cursor image. - pixel = transparent; + auto bytes = shape_info.Pitch * shape_info.Height; + auto pixel_begin = (std::uint32_t *) std::begin(cursor_img); + auto pixel_data = pixel_begin; + auto and_mask = std::begin(img_data); + auto xor_mask = std::begin(img_data) + bytes; + + for (auto x = 0; x < bytes; ++x) { + for (auto c = 7; c >= 0; --c) { + auto bit = 1 << c; + auto color_type = ((*and_mask & bit) ? 1 : 0) + ((*xor_mask & bit) ? 2 : 0); + + switch (color_type) { + case 0: // Opaque black + *pixel_data = black; + break; + case 2: // Opaque white + *pixel_data = white; + break; + case 3: // Inverse of screen (handled by XOR blending) + case 1: // Color of screen (transparent) + *pixel_data = transparent; + break; + } + + ++pixel_data; } - else if(alpha == 0x00) { - // Pixels with 0x00 alpha will be blended as opaque with the alpha-blended image. - pixel |= 0xFF000000; - } - else { - // Other alpha values are illegal in masked color cursors - BOOST_LOG(warning) << "Illegal alpha value in masked color cursor: " << alpha; - } - }); + ++and_mask; + ++xor_mask; + } + return cursor_img; } - case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR: - // Color cursors are just an ARGB bitmap which requires no processing. - return img_data; - case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME: - // Monochrome cursors are handled below. - break; - default: - BOOST_LOG(error) << "Invalid cursor shape type: " << shape_info.Type; - return {}; - } - shape_info.Height /= 2; + blob_t + compile_shader(LPCSTR file, LPCSTR entrypoint, LPCSTR shader_model) { + blob_t::pointer msg_p = nullptr; + blob_t::pointer compiled_p; - util::buffer_t cursor_img { shape_info.Width * shape_info.Height * 4 }; - - auto bytes = shape_info.Pitch * shape_info.Height; - auto pixel_begin = (std::uint32_t *)std::begin(cursor_img); - auto pixel_data = pixel_begin; - auto and_mask = std::begin(img_data); - auto xor_mask = std::begin(img_data) + bytes; - - for(auto x = 0; x < bytes; ++x) { - for(auto c = 7; c >= 0; --c) { - auto bit = 1 << c; - auto color_type = ((*and_mask & bit) ? 1 : 0) + ((*xor_mask & bit) ? 2 : 0); - - switch(color_type) { - case 0: // Opaque black - *pixel_data = black; - break; - case 2: // Opaque white - *pixel_data = white; - break; - case 3: // Inverse of screen (handled by XOR blending) - case 1: // Color of screen (transparent) - *pixel_data = transparent; - break; - } - - ++pixel_data; - } - ++and_mask; - ++xor_mask; - } - - return cursor_img; -} - -blob_t compile_shader(LPCSTR file, LPCSTR entrypoint, LPCSTR shader_model) { - blob_t::pointer msg_p = nullptr; - blob_t::pointer compiled_p; - - DWORD flags = D3DCOMPILE_ENABLE_STRICTNESS; + DWORD flags = D3DCOMPILE_ENABLE_STRICTNESS; #ifndef NDEBUG - flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; + flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; #endif - std::wstring_convert, wchar_t> converter; + std::wstring_convert, wchar_t> converter; - auto wFile = converter.from_bytes(file); - auto status = D3DCompileFromFile(wFile.c_str(), nullptr, nullptr, entrypoint, shader_model, flags, 0, &compiled_p, &msg_p); + auto wFile = converter.from_bytes(file); + auto status = D3DCompileFromFile(wFile.c_str(), nullptr, nullptr, entrypoint, shader_model, flags, 0, &compiled_p, &msg_p); - if(msg_p) { - BOOST_LOG(warning) << std::string_view { (const char *)msg_p->GetBufferPointer(), msg_p->GetBufferSize() - 1 }; - msg_p->Release(); - } - - if(status) { - BOOST_LOG(error) << "Couldn't compile ["sv << file << "] [0x"sv << util::hex(status).to_string_view() << ']'; - return nullptr; - } - - return blob_t { compiled_p }; -} - -blob_t compile_pixel_shader(LPCSTR file) { - return compile_shader(file, "main_ps", "ps_5_0"); -} - -blob_t compile_vertex_shader(LPCSTR file) { - return compile_shader(file, "main_vs", "vs_5_0"); -} - -class hwdevice_t : public platf::hwdevice_t { -public: - int convert(platf::img_t &img_base) override { - auto &img = (img_d3d_t &)img_base; - auto &img_ctx = img_ctx_map[img.id]; - - // Open the shared capture texture with our ID3D11Device - if(initialize_image_context(img, img_ctx)) { - return -1; + if (msg_p) { + BOOST_LOG(warning) << std::string_view { (const char *) msg_p->GetBufferPointer(), msg_p->GetBufferSize() - 1 }; + msg_p->Release(); } - // Acquire encoder mutex to synchronize with capture code - auto status = img_ctx.encoder_mutex->AcquireSync(0, INFINITE); - if(status != S_OK) { - BOOST_LOG(error) << "Failed to acquire encoder mutex [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; + if (status) { + BOOST_LOG(error) << "Couldn't compile ["sv << file << "] [0x"sv << util::hex(status).to_string_view() << ']'; + return nullptr; } - device_ctx->OMSetRenderTargets(1, &nv12_Y_rt, nullptr); - device_ctx->VSSetShader(scene_vs.get(), nullptr, 0); - device_ctx->PSSetShader(convert_Y_ps.get(), nullptr, 0); - device_ctx->RSSetViewports(1, &outY_view); - device_ctx->PSSetShaderResources(0, 1, &img_ctx.encoder_input_res); - device_ctx->Draw(3, 0); - - // Artifacts start appearing on the rendered image if Sunshine doesn't flush - // before rendering on the UV part of the image. - device_ctx->Flush(); - - device_ctx->OMSetRenderTargets(1, &nv12_UV_rt, nullptr); - device_ctx->VSSetShader(convert_UV_vs.get(), nullptr, 0); - device_ctx->PSSetShader(convert_UV_ps.get(), nullptr, 0); - device_ctx->RSSetViewports(1, &outUV_view); - device_ctx->Draw(3, 0); - - // Release encoder mutex to allow capture code to reuse this image - img_ctx.encoder_mutex->ReleaseSync(0); - - return 0; + return blob_t { compiled_p }; } - void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override { - switch(colorspace) { - case 5: // SWS_CS_SMPTE170M - color_p = &::video::colors[0]; - break; - case 1: // SWS_CS_ITU709 - color_p = &::video::colors[2]; - break; - case 9: // SWS_CS_BT2020 - color_p = &::video::colors[4]; - break; - default: - BOOST_LOG(warning) << "Colorspace: ["sv << colorspace << "] not yet supported: switching to default"sv; - color_p = &::video::colors[0]; + blob_t + compile_pixel_shader(LPCSTR file) { + return compile_shader(file, "main_ps", "ps_5_0"); + } + + blob_t + compile_vertex_shader(LPCSTR file) { + return compile_shader(file, "main_vs", "vs_5_0"); + } + + class hwdevice_t: public platf::hwdevice_t { + public: + int + convert(platf::img_t &img_base) override { + auto &img = (img_d3d_t &) img_base; + auto &img_ctx = img_ctx_map[img.id]; + + // Open the shared capture texture with our ID3D11Device + if (initialize_image_context(img, img_ctx)) { + return -1; + } + + // Acquire encoder mutex to synchronize with capture code + auto status = img_ctx.encoder_mutex->AcquireSync(0, INFINITE); + if (status != S_OK) { + BOOST_LOG(error) << "Failed to acquire encoder mutex [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + device_ctx->OMSetRenderTargets(1, &nv12_Y_rt, nullptr); + device_ctx->VSSetShader(scene_vs.get(), nullptr, 0); + device_ctx->PSSetShader(convert_Y_ps.get(), nullptr, 0); + device_ctx->RSSetViewports(1, &outY_view); + device_ctx->PSSetShaderResources(0, 1, &img_ctx.encoder_input_res); + device_ctx->Draw(3, 0); + + // Artifacts start appearing on the rendered image if Sunshine doesn't flush + // before rendering on the UV part of the image. + device_ctx->Flush(); + + device_ctx->OMSetRenderTargets(1, &nv12_UV_rt, nullptr); + device_ctx->VSSetShader(convert_UV_vs.get(), nullptr, 0); + device_ctx->PSSetShader(convert_UV_ps.get(), nullptr, 0); + device_ctx->RSSetViewports(1, &outUV_view); + device_ctx->Draw(3, 0); + + // Release encoder mutex to allow capture code to reuse this image + img_ctx.encoder_mutex->ReleaseSync(0); + + return 0; + } + + void + set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override { + switch (colorspace) { + case 5: // SWS_CS_SMPTE170M + color_p = &::video::colors[0]; + break; + case 1: // SWS_CS_ITU709 + color_p = &::video::colors[2]; + break; + case 9: // SWS_CS_BT2020 + color_p = &::video::colors[4]; + break; + default: + BOOST_LOG(warning) << "Colorspace: ["sv << colorspace << "] not yet supported: switching to default"sv; + color_p = &::video::colors[0]; + }; + + if (color_range > 1) { + // Full range + ++color_p; + } + + auto color_matrix = make_buffer((device_t::pointer) data, *color_p); + if (!color_matrix) { + BOOST_LOG(warning) << "Failed to create color matrix"sv; + return; + } + + device_ctx->VSSetConstantBuffers(0, 1, &info_scene); + device_ctx->PSSetConstantBuffers(0, 1, &color_matrix); + this->color_matrix = std::move(color_matrix); + } + + void + init_hwframes(AVHWFramesContext *frames) override { + // We may be called with a QSV or D3D11VA context + if (frames->device_ctx->type == AV_HWDEVICE_TYPE_D3D11VA) { + auto d3d11_frames = (AVD3D11VAFramesContext *) frames->hwctx; + + // The encoder requires textures with D3D11_BIND_RENDER_TARGET set + d3d11_frames->BindFlags = D3D11_BIND_RENDER_TARGET; + d3d11_frames->MiscFlags = 0; + } + + // We require a single texture + frames->initial_pool_size = 1; + } + + int + prepare_to_derive_context(int hw_device_type) override { + // QuickSync requires our device to be multithread-protected + if (hw_device_type == AV_HWDEVICE_TYPE_QSV) { + multithread_t mt; + + auto status = device->QueryInterface(IID_ID3D11Multithread, (void **) &mt); + if (FAILED(status)) { + BOOST_LOG(warning) << "Failed to query ID3D11Multithread interface from device [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + mt->SetMultithreadProtected(TRUE); + } + + return 0; + } + + int + set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { + this->hwframe.reset(frame); + this->frame = frame; + + // Populate this frame with a hardware buffer if one isn't there already + if (!frame->buf[0]) { + auto err = av_hwframe_get_buffer(hw_frames_ctx, frame, 0); + if (err) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + BOOST_LOG(error) << "Failed to get hwframe buffer: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + return -1; + } + } + + // If this is a frame from a derived context, we'll need to map it to D3D11 + ID3D11Texture2D *frame_texture; + if (frame->format != AV_PIX_FMT_D3D11) { + frame_t d3d11_frame { av_frame_alloc() }; + + d3d11_frame->format = AV_PIX_FMT_D3D11; + + auto err = av_hwframe_map(d3d11_frame.get(), frame, AV_HWFRAME_MAP_WRITE | AV_HWFRAME_MAP_OVERWRITE); + if (err) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + BOOST_LOG(error) << "Failed to map D3D11 frame: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + return -1; + } + + // Get the texture from the mapped frame + frame_texture = (ID3D11Texture2D *) d3d11_frame->data[0]; + } + else { + // Otherwise, we can just use the texture inside the original frame + frame_texture = (ID3D11Texture2D *) frame->data[0]; + } + + auto out_width = frame->width; + auto out_height = frame->height; + + float in_width = display->width; + float in_height = display->height; + + // Ensure aspect ratio is maintained + auto scalar = std::fminf(out_width / in_width, out_height / in_height); + auto out_width_f = in_width * scalar; + auto out_height_f = in_height * scalar; + + // result is always positive + auto offsetX = (out_width - out_width_f) / 2; + auto offsetY = (out_height - out_height_f) / 2; + + outY_view = D3D11_VIEWPORT { offsetX, offsetY, out_width_f, out_height_f, 0.0f, 1.0f }; + outUV_view = D3D11_VIEWPORT { offsetX / 2, offsetY / 2, out_width_f / 2, out_height_f / 2, 0.0f, 1.0f }; + + // The underlying frame pool owns the texture, so we must reference it for ourselves + frame_texture->AddRef(); + hwframe_texture.reset(frame_texture); + + float info_in[16 / sizeof(float)] { 1.0f / (float) out_width_f }; //aligned to 16-byte + info_scene = make_buffer(device.get(), info_in); + + if (!info_scene) { + BOOST_LOG(error) << "Failed to create info scene buffer"sv; + return -1; + } + + D3D11_RENDER_TARGET_VIEW_DESC nv12_rt_desc { + format == DXGI_FORMAT_P010 ? DXGI_FORMAT_R16_UNORM : DXGI_FORMAT_R8_UNORM, + D3D11_RTV_DIMENSION_TEXTURE2D + }; + + auto status = device->CreateRenderTargetView(hwframe_texture.get(), &nv12_rt_desc, &nv12_Y_rt); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + nv12_rt_desc.Format = (format == DXGI_FORMAT_P010) ? DXGI_FORMAT_R16G16_UNORM : DXGI_FORMAT_R8G8_UNORM; + + status = device->CreateRenderTargetView(hwframe_texture.get(), &nv12_rt_desc, &nv12_UV_rt); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + // Clear the RTVs to ensure the aspect ratio padding is black + const float y_black[] = { 0.0f, 0.0f, 0.0f, 0.0f }; + device_ctx->ClearRenderTargetView(nv12_Y_rt.get(), y_black); + const float uv_black[] = { 0.5f, 0.5f, 0.5f, 0.5f }; + device_ctx->ClearRenderTargetView(nv12_UV_rt.get(), uv_black); + + return 0; + } + + int + init( + std::shared_ptr display, adapter_t::pointer adapter_p, + pix_fmt_e pix_fmt) { + D3D_FEATURE_LEVEL featureLevels[] { + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL_9_1 + }; + + HRESULT status = D3D11CreateDevice( + adapter_p, + D3D_DRIVER_TYPE_UNKNOWN, + nullptr, + D3D11_CREATE_DEVICE_FLAGS, + featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), + D3D11_SDK_VERSION, + &device, + nullptr, + &device_ctx); + + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create encoder D3D11 device [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + dxgi::dxgi_t dxgi; + status = device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi); + if (FAILED(status)) { + BOOST_LOG(warning) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + status = dxgi->SetGPUThreadPriority(7); + if (FAILED(status)) { + BOOST_LOG(warning) << "Failed to increase encoding GPU thread priority. Please run application as administrator for optimal performance."; + } + + data = device.get(); + + format = (pix_fmt == pix_fmt_e::nv12 ? DXGI_FORMAT_NV12 : DXGI_FORMAT_P010); + status = device->CreateVertexShader(scene_vs_hlsl->GetBufferPointer(), scene_vs_hlsl->GetBufferSize(), nullptr, &scene_vs); + if (status) { + BOOST_LOG(error) << "Failed to create scene vertex shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + status = device->CreateVertexShader(convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(), nullptr, &convert_UV_vs); + if (status) { + BOOST_LOG(error) << "Failed to create convertUV vertex shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + // If the display is in HDR and we're streaming HDR, we'll be converting scRGB to SMPTE 2084 PQ. + // NB: We can consume scRGB in SDR with our regular shaders because it behaves like UNORM input. + if (format == DXGI_FORMAT_P010 && display->is_hdr()) { + status = device->CreatePixelShader(convert_Y_PQ_ps_hlsl->GetBufferPointer(), convert_Y_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps); + if (status) { + BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + status = device->CreatePixelShader(convert_UV_PQ_ps_hlsl->GetBufferPointer(), convert_UV_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps); + if (status) { + BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + } + else { + status = device->CreatePixelShader(convert_Y_ps_hlsl->GetBufferPointer(), convert_Y_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps); + if (status) { + BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + status = device->CreatePixelShader(convert_UV_ps_hlsl->GetBufferPointer(), convert_UV_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps); + if (status) { + BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + } + + color_matrix = make_buffer(device.get(), ::video::colors[0]); + if (!color_matrix) { + BOOST_LOG(error) << "Failed to create color matrix buffer"sv; + return -1; + } + + D3D11_INPUT_ELEMENT_DESC layout_desc { + "SV_Position", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 + }; + + status = device->CreateInputLayout( + &layout_desc, 1, + convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(), + &input_layout); + + this->display = std::move(display); + + blend_disable = make_blend(device.get(), false, false); + if (!blend_disable) { + return -1; + } + + D3D11_SAMPLER_DESC sampler_desc {}; + sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP; + sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER; + sampler_desc.MinLOD = 0; + sampler_desc.MaxLOD = D3D11_FLOAT32_MAX; + + status = device->CreateSamplerState(&sampler_desc, &sampler_linear); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create point sampler state [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + device_ctx->IASetInputLayout(input_layout.get()); + device_ctx->PSSetConstantBuffers(0, 1, &color_matrix); + device_ctx->VSSetConstantBuffers(0, 1, &info_scene); + + device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu); + device_ctx->PSSetSamplers(0, 1, &sampler_linear); + device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + + return 0; + } + + private: + struct encoder_img_ctx_t { + // Used to determine if the underlying texture changes. + // Not safe for actual use by the encoder! + texture2d_t::pointer capture_texture_p; + + texture2d_t encoder_texture; + shader_res_t encoder_input_res; + keyed_mutex_t encoder_mutex; + + void + reset() { + capture_texture_p = nullptr; + encoder_texture.reset(); + encoder_input_res.reset(); + encoder_mutex.reset(); + } }; - if(color_range > 1) { - // Full range - ++color_p; - } + int + initialize_image_context(const img_d3d_t &img, encoder_img_ctx_t &img_ctx) { + // If we've already opened the shared texture, we're done + if (img_ctx.encoder_texture && img.capture_texture.get() == img_ctx.capture_texture_p) { + return 0; + } - auto color_matrix = make_buffer((device_t::pointer)data, *color_p); - if(!color_matrix) { - BOOST_LOG(warning) << "Failed to create color matrix"sv; - return; - } + // Reset this image context in case it was used before with a different texture. + // Textures can change when transitioning from a dummy image to a real image. + img_ctx.reset(); - device_ctx->VSSetConstantBuffers(0, 1, &info_scene); - device_ctx->PSSetConstantBuffers(0, 1, &color_matrix); - this->color_matrix = std::move(color_matrix); - } - - void init_hwframes(AVHWFramesContext *frames) override { - // We may be called with a QSV or D3D11VA context - if(frames->device_ctx->type == AV_HWDEVICE_TYPE_D3D11VA) { - auto d3d11_frames = (AVD3D11VAFramesContext *)frames->hwctx; - - // The encoder requires textures with D3D11_BIND_RENDER_TARGET set - d3d11_frames->BindFlags = D3D11_BIND_RENDER_TARGET; - d3d11_frames->MiscFlags = 0; - } - - // We require a single texture - frames->initial_pool_size = 1; - } - - int prepare_to_derive_context(int hw_device_type) override { - // QuickSync requires our device to be multithread-protected - if(hw_device_type == AV_HWDEVICE_TYPE_QSV) { - multithread_t mt; - - auto status = device->QueryInterface(IID_ID3D11Multithread, (void **)&mt); - if(FAILED(status)) { - BOOST_LOG(warning) << "Failed to query ID3D11Multithread interface from device [0x"sv << util::hex(status).to_string_view() << ']'; + device1_t device1; + auto status = device->QueryInterface(__uuidof(ID3D11Device1), (void **) &device1); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to query ID3D11Device1 [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - mt->SetMultithreadProtected(TRUE); + // Open a handle to the shared texture + status = device1->OpenSharedResource1(img.encoder_texture_handle, __uuidof(ID3D11Texture2D), (void **) &img_ctx.encoder_texture); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to open shared image texture [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + // Get the keyed mutex to synchronize with the capture code + status = img_ctx.encoder_texture->QueryInterface(__uuidof(IDXGIKeyedMutex), (void **) &img_ctx.encoder_mutex); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to query IDXGIKeyedMutex [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + // Create the SRV for the encoder texture + status = device->CreateShaderResourceView(img_ctx.encoder_texture.get(), nullptr, &img_ctx.encoder_input_res); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create shader resource view for encoding [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + img_ctx.capture_texture_p = img.capture_texture.get(); + return 0; } - return 0; + public: + frame_t hwframe; + + ::video::color_t *color_p; + + buf_t info_scene; + buf_t color_matrix; + + input_layout_t input_layout; + + blend_t blend_disable; + sampler_state_t sampler_linear; + + render_target_t nv12_Y_rt; + render_target_t nv12_UV_rt; + + // The image referenced by hwframe + texture2d_t hwframe_texture; + + // d3d_img_t::id -> encoder_img_ctx_t + // These store the encoder textures for each img_t that passes through + // convert(). We can't store them in the img_t itself because it is shared + // amongst multiple hwdevice_t objects (and therefore multiple ID3D11Devices). + std::map img_ctx_map; + + std::shared_ptr display; + + vs_t convert_UV_vs; + ps_t convert_UV_ps; + ps_t convert_Y_ps; + vs_t scene_vs; + + D3D11_VIEWPORT outY_view; + D3D11_VIEWPORT outUV_view; + + DXGI_FORMAT format; + + device_t device; + device_ctx_t device_ctx; + }; + + bool + set_cursor_texture(device_t::pointer device, gpu_cursor_t &cursor, util::buffer_t &&cursor_img, DXGI_OUTDUPL_POINTER_SHAPE_INFO &shape_info) { + // This cursor image may not be used + if (cursor_img.size() == 0) { + cursor.input_res.reset(); + cursor.set_texture(0, 0, nullptr); + return true; + } + + D3D11_SUBRESOURCE_DATA data { + std::begin(cursor_img), + 4 * shape_info.Width, + 0 + }; + + // Create texture for cursor + D3D11_TEXTURE2D_DESC t {}; + t.Width = shape_info.Width; + t.Height = cursor_img.size() / data.SysMemPitch; + t.MipLevels = 1; + t.ArraySize = 1; + t.SampleDesc.Count = 1; + t.Usage = D3D11_USAGE_IMMUTABLE; + t.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + t.BindFlags = D3D11_BIND_SHADER_RESOURCE; + + texture2d_t texture; + auto status = device->CreateTexture2D(&t, &data, &texture); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create mouse texture [0x"sv << util::hex(status).to_string_view() << ']'; + return false; + } + + // Free resources before allocating on the next line. + cursor.input_res.reset(); + status = device->CreateShaderResourceView(texture.get(), nullptr, &cursor.input_res); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create cursor shader resource view [0x"sv << util::hex(status).to_string_view() << ']'; + return false; + } + + cursor.set_texture(t.Width, t.Height, std::move(texture)); + return true; } - int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { - this->hwframe.reset(frame); - this->frame = frame; + capture_e + display_vram_t::snapshot(platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) { + auto img = (img_d3d_t *) img_base; - // Populate this frame with a hardware buffer if one isn't there already - if(!frame->buf[0]) { - auto err = av_hwframe_get_buffer(hw_frames_ctx, frame, 0); - if(err) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) << "Failed to get hwframe buffer: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); - return -1; + HRESULT status; + + DXGI_OUTDUPL_FRAME_INFO frame_info; + + resource_t::pointer res_p {}; + auto capture_status = dup.next_frame(frame_info, timeout, &res_p); + resource_t res { res_p }; + + if (capture_status != capture_e::ok) { + return capture_status; + } + + const bool mouse_update_flag = frame_info.LastMouseUpdateTime.QuadPart != 0 || frame_info.PointerShapeBufferSize > 0; + const bool frame_update_flag = frame_info.AccumulatedFrames != 0 || frame_info.LastPresentTime.QuadPart != 0; + const bool update_flag = mouse_update_flag || frame_update_flag; + + if (!update_flag) { + return capture_e::timeout; + } + + if (frame_info.PointerShapeBufferSize > 0) { + DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info {}; + + util::buffer_t img_data { frame_info.PointerShapeBufferSize }; + + UINT dummy; + status = dup.dup->GetFramePointerShape(img_data.size(), std::begin(img_data), &dummy, &shape_info); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to get new pointer shape [0x"sv << util::hex(status).to_string_view() << ']'; + + return capture_e::error; + } + + auto alpha_cursor_img = make_cursor_alpha_image(img_data, shape_info); + auto xor_cursor_img = make_cursor_xor_image(img_data, shape_info); + + if (!set_cursor_texture(device.get(), cursor_alpha, std::move(alpha_cursor_img), shape_info) || + !set_cursor_texture(device.get(), cursor_xor, std::move(xor_cursor_img), shape_info)) { + return capture_e::error; } } - // If this is a frame from a derived context, we'll need to map it to D3D11 - ID3D11Texture2D *frame_texture; - if(frame->format != AV_PIX_FMT_D3D11) { - frame_t d3d11_frame { av_frame_alloc() }; + if (frame_info.LastMouseUpdateTime.QuadPart) { + cursor_alpha.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible); + cursor_xor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible); + } - d3d11_frame->format = AV_PIX_FMT_D3D11; + if (frame_update_flag) { + texture2d_t src {}; - auto err = av_hwframe_map(d3d11_frame.get(), frame, AV_HWFRAME_MAP_WRITE | AV_HWFRAME_MAP_OVERWRITE); - if(err) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) << "Failed to map D3D11 frame: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); - return -1; + // Get the texture object from this frame + status = res->QueryInterface(IID_ID3D11Texture2D, (void **) &src); + if (FAILED(status)) { + BOOST_LOG(error) << "Couldn't query interface [0x"sv << util::hex(status).to_string_view() << ']'; + return capture_e::error; } - // Get the texture from the mapped frame - frame_texture = (ID3D11Texture2D *)d3d11_frame->data[0]; + D3D11_TEXTURE2D_DESC desc; + src->GetDesc(&desc); + + // It's possible for our display enumeration to race with mode changes and result in + // mismatched image pool and desktop texture sizes. If this happens, just reinit again. + if (desc.Width != width || desc.Height != height) { + BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; + return capture_e::reinit; + } + + // If we don't know the capture format yet, grab it from this texture + if (capture_format == DXGI_FORMAT_UNKNOWN) { + capture_format = desc.Format; + BOOST_LOG(info) << "Capture format ["sv << dxgi_format_to_string(capture_format) << ']'; + + D3D11_TEXTURE2D_DESC t {}; + t.Width = width; + t.Height = height; + t.MipLevels = 1; + t.ArraySize = 1; + t.SampleDesc.Count = 1; + t.Usage = D3D11_USAGE_DEFAULT; + t.Format = capture_format; + t.BindFlags = 0; + + // Create a texture to store the most recent copy of the desktop + status = device->CreateTexture2D(&t, nullptr, &last_frame_copy); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create frame copy texture [0x"sv << util::hex(status).to_string_view() << ']'; + return capture_e::error; + } + } + + // It's also possible for the capture format to change on the fly. If that happens, + // reinitialize capture to try format detection again and create new images. + if (capture_format != desc.Format) { + BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; + return capture_e::reinit; + } + + // Now that we know the capture format, we can finish creating the image + if (complete_img(img, false)) { + return capture_e::error; + } + + // Copy the texture to use for cursor-only updates + device_ctx->CopyResource(last_frame_copy.get(), src.get()); + + // Copy into the capture texture on the image with the mutex held + status = img->capture_mutex->AcquireSync(0, INFINITE); + if (status != S_OK) { + BOOST_LOG(error) << "Failed to acquire capture mutex [0x"sv << util::hex(status).to_string_view() << ']'; + return capture_e::error; + } + device_ctx->CopyResource(img->capture_texture.get(), src.get()); + } + else if (capture_format == DXGI_FORMAT_UNKNOWN) { + // We don't know the final capture format yet, so we will encode a dummy image + BOOST_LOG(debug) << "Capture format is still unknown. Encoding a blank image"sv; + + // Finish creating the image as a dummy (if it hasn't happened already) + if (complete_img(img, true)) { + return capture_e::error; + } + + auto dummy_data = std::make_unique(img->row_pitch * img->height); + std::fill_n(dummy_data.get(), img->row_pitch * img->height, 0); + + status = img->capture_mutex->AcquireSync(0, INFINITE); + if (status != S_OK) { + BOOST_LOG(error) << "Failed to acquire capture mutex [0x"sv << util::hex(status).to_string_view() << ']'; + return capture_e::error; + } + + // Populate the image with dummy data. This is required because these images could be reused + // after rendering (in which case they would have a cursor already rendered into them). + device_ctx->UpdateSubresource(img->capture_texture.get(), 0, nullptr, dummy_data.get(), img->row_pitch, 0); } else { - // Otherwise, we can just use the texture inside the original frame - frame_texture = (ID3D11Texture2D *)frame->data[0]; + // We must know the capture format in this path or we would have hit the above unknown format case + if (complete_img(img, false)) { + return capture_e::error; + } + + // We have a previously captured frame to reuse. We can't just grab the src texture from + // the call to AcquireNextFrame() because that won't be valid. It seems to return a texture + // in the unmodified desktop format (rather than the formats we passed to DuplicateOutput1()) + // if called in that case. + status = img->capture_mutex->AcquireSync(0, INFINITE); + if (status != S_OK) { + BOOST_LOG(error) << "Failed to acquire capture mutex [0x"sv << util::hex(status).to_string_view() << ']'; + return capture_e::error; + } + device_ctx->CopyResource(img->capture_texture.get(), last_frame_copy.get()); } - auto out_width = frame->width; - auto out_height = frame->height; + if ((cursor_alpha.visible || cursor_xor.visible) && cursor_visible) { + device_ctx->VSSetShader(scene_vs.get(), nullptr, 0); + device_ctx->PSSetShader(scene_ps.get(), nullptr, 0); + device_ctx->OMSetRenderTargets(1, &img->capture_rt, nullptr); - float in_width = display->width; - float in_height = display->height; + if (cursor_alpha.texture.get()) { + // Perform an alpha blending operation + device_ctx->OMSetBlendState(blend_alpha.get(), nullptr, 0xFFFFFFFFu); - // Ensure aspect ratio is maintained - auto scalar = std::fminf(out_width / in_width, out_height / in_height); - auto out_width_f = in_width * scalar; - auto out_height_f = in_height * scalar; + device_ctx->PSSetShaderResources(0, 1, &cursor_alpha.input_res); + device_ctx->RSSetViewports(1, &cursor_alpha.cursor_view); + device_ctx->Draw(3, 0); + } - // result is always positive - auto offsetX = (out_width - out_width_f) / 2; - auto offsetY = (out_height - out_height_f) / 2; + if (cursor_xor.texture.get()) { + // Perform an invert blending without touching alpha values + device_ctx->OMSetBlendState(blend_invert.get(), nullptr, 0x00FFFFFFu); - outY_view = D3D11_VIEWPORT { offsetX, offsetY, out_width_f, out_height_f, 0.0f, 1.0f }; - outUV_view = D3D11_VIEWPORT { offsetX / 2, offsetY / 2, out_width_f / 2, out_height_f / 2, 0.0f, 1.0f }; + device_ctx->PSSetShaderResources(0, 1, &cursor_xor.input_res); + device_ctx->RSSetViewports(1, &cursor_xor.cursor_view); + device_ctx->Draw(3, 0); + } - // The underlying frame pool owns the texture, so we must reference it for ourselves - frame_texture->AddRef(); - hwframe_texture.reset(frame_texture); - - float info_in[16 / sizeof(float)] { 1.0f / (float)out_width_f }; //aligned to 16-byte - info_scene = make_buffer(device.get(), info_in); - - if(!info_scene) { - BOOST_LOG(error) << "Failed to create info scene buffer"sv; - return -1; + device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu); } - D3D11_RENDER_TARGET_VIEW_DESC nv12_rt_desc { - format == DXGI_FORMAT_P010 ? DXGI_FORMAT_R16_UNORM : DXGI_FORMAT_R8_UNORM, - D3D11_RTV_DIMENSION_TEXTURE2D - }; + // Release the mutex to allow encoding of this frame + img->capture_mutex->ReleaseSync(0); - auto status = device->CreateRenderTargetView(hwframe_texture.get(), &nv12_rt_desc, &nv12_Y_rt); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - nv12_rt_desc.Format = (format == DXGI_FORMAT_P010) ? DXGI_FORMAT_R16G16_UNORM : DXGI_FORMAT_R8G8_UNORM; - - status = device->CreateRenderTargetView(hwframe_texture.get(), &nv12_rt_desc, &nv12_UV_rt); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - // Clear the RTVs to ensure the aspect ratio padding is black - const float y_black[] = { 0.0f, 0.0f, 0.0f, 0.0f }; - device_ctx->ClearRenderTargetView(nv12_Y_rt.get(), y_black); - const float uv_black[] = { 0.5f, 0.5f, 0.5f, 0.5f }; - device_ctx->ClearRenderTargetView(nv12_UV_rt.get(), uv_black); - - return 0; + return capture_e::ok; } - int init( - std::shared_ptr display, adapter_t::pointer adapter_p, - pix_fmt_e pix_fmt) { - - D3D_FEATURE_LEVEL featureLevels[] { - D3D_FEATURE_LEVEL_11_1, - D3D_FEATURE_LEVEL_11_0, - D3D_FEATURE_LEVEL_10_1, - D3D_FEATURE_LEVEL_10_0, - D3D_FEATURE_LEVEL_9_3, - D3D_FEATURE_LEVEL_9_2, - D3D_FEATURE_LEVEL_9_1 - }; - - HRESULT status = D3D11CreateDevice( - adapter_p, - D3D_DRIVER_TYPE_UNKNOWN, - nullptr, - D3D11_CREATE_DEVICE_FLAGS, - featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), - D3D11_SDK_VERSION, - &device, - nullptr, - &device_ctx); - - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create encoder D3D11 device [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - dxgi::dxgi_t dxgi; - status = device->QueryInterface(IID_IDXGIDevice, (void **)&dxgi); - if(FAILED(status)) { - BOOST_LOG(warning) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - status = dxgi->SetGPUThreadPriority(7); - if(FAILED(status)) { - BOOST_LOG(warning) << "Failed to increase encoding GPU thread priority. Please run application as administrator for optimal performance."; - } - - data = device.get(); - - format = (pix_fmt == pix_fmt_e::nv12 ? DXGI_FORMAT_NV12 : DXGI_FORMAT_P010); - status = device->CreateVertexShader(scene_vs_hlsl->GetBufferPointer(), scene_vs_hlsl->GetBufferSize(), nullptr, &scene_vs); - if(status) { - BOOST_LOG(error) << "Failed to create scene vertex shader [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - status = device->CreateVertexShader(convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(), nullptr, &convert_UV_vs); - if(status) { - BOOST_LOG(error) << "Failed to create convertUV vertex shader [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - // If the display is in HDR and we're streaming HDR, we'll be converting scRGB to SMPTE 2084 PQ. - // NB: We can consume scRGB in SDR with our regular shaders because it behaves like UNORM input. - if(format == DXGI_FORMAT_P010 && display->is_hdr()) { - status = device->CreatePixelShader(convert_Y_PQ_ps_hlsl->GetBufferPointer(), convert_Y_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps); - if(status) { - BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - status = device->CreatePixelShader(convert_UV_PQ_ps_hlsl->GetBufferPointer(), convert_UV_PQ_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps); - if(status) { - BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - } - else { - status = device->CreatePixelShader(convert_Y_ps_hlsl->GetBufferPointer(), convert_Y_ps_hlsl->GetBufferSize(), nullptr, &convert_Y_ps); - if(status) { - BOOST_LOG(error) << "Failed to create convertY pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - status = device->CreatePixelShader(convert_UV_ps_hlsl->GetBufferPointer(), convert_UV_ps_hlsl->GetBufferSize(), nullptr, &convert_UV_ps); - if(status) { - BOOST_LOG(error) << "Failed to create convertUV pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - } - - color_matrix = make_buffer(device.get(), ::video::colors[0]); - if(!color_matrix) { - BOOST_LOG(error) << "Failed to create color matrix buffer"sv; - return -1; - } - - D3D11_INPUT_ELEMENT_DESC layout_desc { - "SV_Position", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 - }; - - status = device->CreateInputLayout( - &layout_desc, 1, - convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(), - &input_layout); - - this->display = std::move(display); - - blend_disable = make_blend(device.get(), false, false); - if(!blend_disable) { + int + display_vram_t::init(const ::video::config_t &config, const std::string &display_name) { + if (display_base_t::init(config, display_name)) { return -1; } D3D11_SAMPLER_DESC sampler_desc {}; - sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; - sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; - sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; - sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP; + sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP; sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER; - sampler_desc.MinLOD = 0; - sampler_desc.MaxLOD = D3D11_FLOAT32_MAX; + sampler_desc.MinLOD = 0; + sampler_desc.MaxLOD = D3D11_FLOAT32_MAX; - status = device->CreateSamplerState(&sampler_desc, &sampler_linear); - if(FAILED(status)) { + auto status = device->CreateSamplerState(&sampler_desc, &sampler_linear); + if (FAILED(status)) { BOOST_LOG(error) << "Failed to create point sampler state [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - device_ctx->IASetInputLayout(input_layout.get()); - device_ctx->PSSetConstantBuffers(0, 1, &color_matrix); - device_ctx->VSSetConstantBuffers(0, 1, &info_scene); + status = device->CreateVertexShader(scene_vs_hlsl->GetBufferPointer(), scene_vs_hlsl->GetBufferSize(), nullptr, &scene_vs); + if (status) { + BOOST_LOG(error) << "Failed to create scene vertex shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + if (config.dynamicRange && is_hdr()) { + // This shader will normalize scRGB white levels to a user-defined white level + status = device->CreatePixelShader(scene_NW_ps_hlsl->GetBufferPointer(), scene_NW_ps_hlsl->GetBufferSize(), nullptr, &scene_ps); + if (status) { + BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + // Use a 300 nit target for the mouse cursor. We should really get + // the user's SDR white level in nits, but there is no API that + // provides that information to Win32 apps. + float sdr_multiplier_data[16 / sizeof(float)] { 300.0f / 80.f }; // aligned to 16-byte + auto sdr_multiplier = make_buffer(device.get(), sdr_multiplier_data); + if (!sdr_multiplier) { + BOOST_LOG(warning) << "Failed to create SDR multiplier"sv; + return -1; + } + + device_ctx->PSSetConstantBuffers(0, 1, &sdr_multiplier); + } + else { + status = device->CreatePixelShader(scene_ps_hlsl->GetBufferPointer(), scene_ps_hlsl->GetBufferSize(), nullptr, &scene_ps); + if (status) { + BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + } + + blend_alpha = make_blend(device.get(), true, false); + blend_invert = make_blend(device.get(), true, true); + blend_disable = make_blend(device.get(), false, false); + + if (!blend_disable || !blend_alpha || !blend_invert) { + return -1; + } device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu); device_ctx->PSSetSamplers(0, 1, &sampler_linear); @@ -632,602 +1051,207 @@ public: return 0; } -private: - struct encoder_img_ctx_t { - // Used to determine if the underlying texture changes. - // Not safe for actual use by the encoder! - texture2d_t::pointer capture_texture_p; + std::shared_ptr + display_vram_t::alloc_img() { + auto img = std::make_shared(); - texture2d_t encoder_texture; - shader_res_t encoder_input_res; - keyed_mutex_t encoder_mutex; + // Initialize format-independent fields + img->width = width; + img->height = height; + img->display = shared_from_this(); + img->id = next_image_id++; - void reset() { - capture_texture_p = nullptr; - encoder_texture.reset(); - encoder_input_res.reset(); - encoder_mutex.reset(); - } - }; + return img; + } - int initialize_image_context(const img_d3d_t &img, encoder_img_ctx_t &img_ctx) { - // If we've already opened the shared texture, we're done - if(img_ctx.encoder_texture && img.capture_texture.get() == img_ctx.capture_texture_p) { + // This cannot use ID3D11DeviceContext because it can be called concurrently by the encoding thread + int + display_vram_t::complete_img(platf::img_t *img_base, bool dummy) { + auto img = (img_d3d_t *) img_base; + + // If this already has a capture texture and it's not switching dummy state, nothing to do + if (img->capture_texture && img->dummy == dummy) { return 0; } - // Reset this image context in case it was used before with a different texture. - // Textures can change when transitioning from a dummy image to a real image. - img_ctx.reset(); - - device1_t device1; - auto status = device->QueryInterface(__uuidof(ID3D11Device1), (void **)&device1); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to query ID3D11Device1 [0x"sv << util::hex(status).to_string_view() << ']'; + // If this is not a dummy image, we must know the format by now + if (!dummy && capture_format == DXGI_FORMAT_UNKNOWN) { + BOOST_LOG(error) << "display_vram_t::complete_img() called with unknown capture format!"; return -1; } - // Open a handle to the shared texture - status = device1->OpenSharedResource1(img.encoder_texture_handle, __uuidof(ID3D11Texture2D), (void **)&img_ctx.encoder_texture); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to open shared image texture [0x"sv << util::hex(status).to_string_view() << ']'; + // Reset the image (in case this was previously a dummy) + img->capture_texture.reset(); + img->capture_rt.reset(); + img->capture_mutex.reset(); + img->data = nullptr; + if (img->encoder_texture_handle) { + CloseHandle(img->encoder_texture_handle); + img->encoder_texture_handle = NULL; + } + + // Initialize format-dependent fields + img->pixel_pitch = get_pixel_pitch(); + img->row_pitch = img->pixel_pitch * img->width; + img->dummy = dummy; + + D3D11_TEXTURE2D_DESC t {}; + t.Width = img->width; + t.Height = img->height; + t.MipLevels = 1; + t.ArraySize = 1; + t.SampleDesc.Count = 1; + t.Usage = D3D11_USAGE_DEFAULT; + t.Format = (capture_format == DXGI_FORMAT_UNKNOWN) ? DXGI_FORMAT_B8G8R8A8_UNORM : capture_format; + t.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; + t.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX; + + auto dummy_data = std::make_unique(img->row_pitch * img->height); + std::fill_n(dummy_data.get(), img->row_pitch * img->height, 0); + D3D11_SUBRESOURCE_DATA initial_data { + dummy_data.get(), + (UINT) img->row_pitch, + 0 + }; + + auto status = device->CreateTexture2D(&t, &initial_data, &img->capture_texture); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create img buf texture [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - // Get the keyed mutex to synchronize with the capture code - status = img_ctx.encoder_texture->QueryInterface(__uuidof(IDXGIKeyedMutex), (void **)&img_ctx.encoder_mutex); - if(FAILED(status)) { + status = device->CreateRenderTargetView(img->capture_texture.get(), nullptr, &img->capture_rt); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + // Get the keyed mutex to synchronize with the encoding code + status = img->capture_texture->QueryInterface(__uuidof(IDXGIKeyedMutex), (void **) &img->capture_mutex); + if (FAILED(status)) { BOOST_LOG(error) << "Failed to query IDXGIKeyedMutex [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - // Create the SRV for the encoder texture - status = device->CreateShaderResourceView(img_ctx.encoder_texture.get(), nullptr, &img_ctx.encoder_input_res); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create shader resource view for encoding [0x"sv << util::hex(status).to_string_view() << ']'; + resource1_t resource; + status = img->capture_texture->QueryInterface(__uuidof(IDXGIResource1), (void **) &resource); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to query IDXGIResource1 [0x"sv << util::hex(status).to_string_view() << ']'; return -1; } - img_ctx.capture_texture_p = img.capture_texture.get(); + // Create a handle for the encoder device to use to open this texture + status = resource->CreateSharedHandle(nullptr, DXGI_SHARED_RESOURCE_READ, nullptr, &img->encoder_texture_handle); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create shared texture handle [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + img->data = (std::uint8_t *) img->capture_texture.get(); + return 0; } -public: - frame_t hwframe; - - ::video::color_t *color_p; - - buf_t info_scene; - buf_t color_matrix; - - input_layout_t input_layout; - - blend_t blend_disable; - sampler_state_t sampler_linear; - - render_target_t nv12_Y_rt; - render_target_t nv12_UV_rt; - - // The image referenced by hwframe - texture2d_t hwframe_texture; - - // d3d_img_t::id -> encoder_img_ctx_t - // These store the encoder textures for each img_t that passes through - // convert(). We can't store them in the img_t itself because it is shared - // amongst multiple hwdevice_t objects (and therefore multiple ID3D11Devices). - std::map img_ctx_map; - - std::shared_ptr display; - - vs_t convert_UV_vs; - ps_t convert_UV_ps; - ps_t convert_Y_ps; - vs_t scene_vs; - - D3D11_VIEWPORT outY_view; - D3D11_VIEWPORT outUV_view; - - DXGI_FORMAT format; - - device_t device; - device_ctx_t device_ctx; -}; - -bool set_cursor_texture(device_t::pointer device, gpu_cursor_t &cursor, util::buffer_t &&cursor_img, DXGI_OUTDUPL_POINTER_SHAPE_INFO &shape_info) { - // This cursor image may not be used - if(cursor_img.size() == 0) { - cursor.input_res.reset(); - cursor.set_texture(0, 0, nullptr); - return true; + // This cannot use ID3D11DeviceContext because it can be called concurrently by the encoding thread + int + display_vram_t::dummy_img(platf::img_t *img_base) { + return complete_img(img_base, true); } - D3D11_SUBRESOURCE_DATA data { - std::begin(cursor_img), - 4 * shape_info.Width, - 0 - }; - - // Create texture for cursor - D3D11_TEXTURE2D_DESC t {}; - t.Width = shape_info.Width; - t.Height = cursor_img.size() / data.SysMemPitch; - t.MipLevels = 1; - t.ArraySize = 1; - t.SampleDesc.Count = 1; - t.Usage = D3D11_USAGE_IMMUTABLE; - t.Format = DXGI_FORMAT_B8G8R8A8_UNORM; - t.BindFlags = D3D11_BIND_SHADER_RESOURCE; - - texture2d_t texture; - auto status = device->CreateTexture2D(&t, &data, &texture); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create mouse texture [0x"sv << util::hex(status).to_string_view() << ']'; - return false; + std::vector + display_vram_t::get_supported_sdr_capture_formats() { + return { DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM }; } - // Free resources before allocating on the next line. - cursor.input_res.reset(); - status = device->CreateShaderResourceView(texture.get(), nullptr, &cursor.input_res); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create cursor shader resource view [0x"sv << util::hex(status).to_string_view() << ']'; - return false; + std::vector + display_vram_t::get_supported_hdr_capture_formats() { + return { + // scRGB FP16 is the desired format for HDR content. This will also handle + // 10-bit SDR displays with the increased precision of FP16 vs 8-bit UNORMs. + DXGI_FORMAT_R16G16B16A16_FLOAT, + + // DXGI_FORMAT_R10G10B10A2_UNORM seems like it might give us frames already + // converted to SMPTE 2084 PQ, however it seems to actually just clamp the + // scRGB FP16 values that DWM is using when the desktop format is scRGB FP16. + // + // If there is a case where the desktop format is really SMPTE 2084 PQ, it + // might make sense to support capturing it without conversion to scRGB, + // but we avoid it for now. + + // We include the 8-bit modes too for when the display is in SDR mode, + // while the client stream is HDR-capable. These UNORM formats behave + // like a degenerate case of scRGB FP16 with values between 0.0f-1.0f. + DXGI_FORMAT_B8G8R8A8_UNORM, + DXGI_FORMAT_R8G8B8A8_UNORM, + }; } - cursor.set_texture(t.Width, t.Height, std::move(texture)); - return true; -} + std::shared_ptr + display_vram_t::make_hwdevice(pix_fmt_e pix_fmt) { + if (pix_fmt != platf::pix_fmt_e::nv12 && pix_fmt != platf::pix_fmt_e::p010) { + BOOST_LOG(error) << "display_vram_t doesn't support pixel format ["sv << from_pix_fmt(pix_fmt) << ']'; -capture_e display_vram_t::snapshot(platf::img_t *img_base, std::chrono::milliseconds timeout, bool cursor_visible) { - auto img = (img_d3d_t *)img_base; + return nullptr; + } - HRESULT status; + auto hwdevice = std::make_shared(); - DXGI_OUTDUPL_FRAME_INFO frame_info; + auto ret = hwdevice->init( + shared_from_this(), + adapter.get(), + pix_fmt); - resource_t::pointer res_p {}; - auto capture_status = dup.next_frame(frame_info, timeout, &res_p); - resource_t res { res_p }; + if (ret) { + return nullptr; + } - if(capture_status != capture_e::ok) { - return capture_status; + return hwdevice; } - const bool mouse_update_flag = frame_info.LastMouseUpdateTime.QuadPart != 0 || frame_info.PointerShapeBufferSize > 0; - const bool frame_update_flag = frame_info.AccumulatedFrames != 0 || frame_info.LastPresentTime.QuadPart != 0; - const bool update_flag = mouse_update_flag || frame_update_flag; - - if(!update_flag) { - return capture_e::timeout; - } - - if(frame_info.PointerShapeBufferSize > 0) { - DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info {}; - - util::buffer_t img_data { frame_info.PointerShapeBufferSize }; - - UINT dummy; - status = dup.dup->GetFramePointerShape(img_data.size(), std::begin(img_data), &dummy, &shape_info); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to get new pointer shape [0x"sv << util::hex(status).to_string_view() << ']'; - - return capture_e::error; - } - - auto alpha_cursor_img = make_cursor_alpha_image(img_data, shape_info); - auto xor_cursor_img = make_cursor_xor_image(img_data, shape_info); - - if(!set_cursor_texture(device.get(), cursor_alpha, std::move(alpha_cursor_img), shape_info) || - !set_cursor_texture(device.get(), cursor_xor, std::move(xor_cursor_img), shape_info)) { - return capture_e::error; - } - } - - if(frame_info.LastMouseUpdateTime.QuadPart) { - cursor_alpha.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible); - cursor_xor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible); - } - - if(frame_update_flag) { - texture2d_t src {}; - - // Get the texture object from this frame - status = res->QueryInterface(IID_ID3D11Texture2D, (void **)&src); - if(FAILED(status)) { - BOOST_LOG(error) << "Couldn't query interface [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; - } - - D3D11_TEXTURE2D_DESC desc; - src->GetDesc(&desc); - - // It's possible for our display enumeration to race with mode changes and result in - // mismatched image pool and desktop texture sizes. If this happens, just reinit again. - if(desc.Width != width || desc.Height != height) { - BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; - return capture_e::reinit; - } - - // If we don't know the capture format yet, grab it from this texture - if(capture_format == DXGI_FORMAT_UNKNOWN) { - capture_format = desc.Format; - BOOST_LOG(info) << "Capture format ["sv << dxgi_format_to_string(capture_format) << ']'; - - D3D11_TEXTURE2D_DESC t {}; - t.Width = width; - t.Height = height; - t.MipLevels = 1; - t.ArraySize = 1; - t.SampleDesc.Count = 1; - t.Usage = D3D11_USAGE_DEFAULT; - t.Format = capture_format; - t.BindFlags = 0; - - // Create a texture to store the most recent copy of the desktop - status = device->CreateTexture2D(&t, nullptr, &last_frame_copy); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create frame copy texture [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; - } - } - - // It's also possible for the capture format to change on the fly. If that happens, - // reinitialize capture to try format detection again and create new images. - if(capture_format != desc.Format) { - BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; - return capture_e::reinit; - } - - // Now that we know the capture format, we can finish creating the image - if(complete_img(img, false)) { - return capture_e::error; - } - - // Copy the texture to use for cursor-only updates - device_ctx->CopyResource(last_frame_copy.get(), src.get()); - - // Copy into the capture texture on the image with the mutex held - status = img->capture_mutex->AcquireSync(0, INFINITE); - if(status != S_OK) { - BOOST_LOG(error) << "Failed to acquire capture mutex [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; - } - device_ctx->CopyResource(img->capture_texture.get(), src.get()); - } - else if(capture_format == DXGI_FORMAT_UNKNOWN) { - // We don't know the final capture format yet, so we will encode a dummy image - BOOST_LOG(debug) << "Capture format is still unknown. Encoding a blank image"sv; - - // Finish creating the image as a dummy (if it hasn't happened already) - if(complete_img(img, true)) { - return capture_e::error; - } - - auto dummy_data = std::make_unique(img->row_pitch * img->height); - std::fill_n(dummy_data.get(), img->row_pitch * img->height, 0); - - status = img->capture_mutex->AcquireSync(0, INFINITE); - if(status != S_OK) { - BOOST_LOG(error) << "Failed to acquire capture mutex [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; - } - - // Populate the image with dummy data. This is required because these images could be reused - // after rendering (in which case they would have a cursor already rendered into them). - device_ctx->UpdateSubresource(img->capture_texture.get(), 0, nullptr, dummy_data.get(), img->row_pitch, 0); - } - else { - // We must know the capture format in this path or we would have hit the above unknown format case - if(complete_img(img, false)) { - return capture_e::error; - } - - // We have a previously captured frame to reuse. We can't just grab the src texture from - // the call to AcquireNextFrame() because that won't be valid. It seems to return a texture - // in the unmodified desktop format (rather than the formats we passed to DuplicateOutput1()) - // if called in that case. - status = img->capture_mutex->AcquireSync(0, INFINITE); - if(status != S_OK) { - BOOST_LOG(error) << "Failed to acquire capture mutex [0x"sv << util::hex(status).to_string_view() << ']'; - return capture_e::error; - } - device_ctx->CopyResource(img->capture_texture.get(), last_frame_copy.get()); - } - - if((cursor_alpha.visible || cursor_xor.visible) && cursor_visible) { - device_ctx->VSSetShader(scene_vs.get(), nullptr, 0); - device_ctx->PSSetShader(scene_ps.get(), nullptr, 0); - device_ctx->OMSetRenderTargets(1, &img->capture_rt, nullptr); - - if(cursor_alpha.texture.get()) { - // Perform an alpha blending operation - device_ctx->OMSetBlendState(blend_alpha.get(), nullptr, 0xFFFFFFFFu); - - device_ctx->PSSetShaderResources(0, 1, &cursor_alpha.input_res); - device_ctx->RSSetViewports(1, &cursor_alpha.cursor_view); - device_ctx->Draw(3, 0); - } - - if(cursor_xor.texture.get()) { - // Perform an invert blending without touching alpha values - device_ctx->OMSetBlendState(blend_invert.get(), nullptr, 0x00FFFFFFu); - - device_ctx->PSSetShaderResources(0, 1, &cursor_xor.input_res); - device_ctx->RSSetViewports(1, &cursor_xor.cursor_view); - device_ctx->Draw(3, 0); - } - - device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu); - } - - // Release the mutex to allow encoding of this frame - img->capture_mutex->ReleaseSync(0); - - return capture_e::ok; -} - -int display_vram_t::init(const ::video::config_t &config, const std::string &display_name) { - if(display_base_t::init(config, display_name)) { - return -1; - } - - D3D11_SAMPLER_DESC sampler_desc {}; - sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; - sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; - sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; - sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP; - sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER; - sampler_desc.MinLOD = 0; - sampler_desc.MaxLOD = D3D11_FLOAT32_MAX; - - auto status = device->CreateSamplerState(&sampler_desc, &sampler_linear); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create point sampler state [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - status = device->CreateVertexShader(scene_vs_hlsl->GetBufferPointer(), scene_vs_hlsl->GetBufferSize(), nullptr, &scene_vs); - if(status) { - BOOST_LOG(error) << "Failed to create scene vertex shader [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - if(config.dynamicRange && is_hdr()) { - // This shader will normalize scRGB white levels to a user-defined white level - status = device->CreatePixelShader(scene_NW_ps_hlsl->GetBufferPointer(), scene_NW_ps_hlsl->GetBufferSize(), nullptr, &scene_ps); - if(status) { - BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + int + init() { + BOOST_LOG(info) << "Compiling shaders..."sv; + scene_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/SceneVS.hlsl"); + if (!scene_vs_hlsl) { return -1; } - // Use a 300 nit target for the mouse cursor. We should really get - // the user's SDR white level in nits, but there is no API that - // provides that information to Win32 apps. - float sdr_multiplier_data[16 / sizeof(float)] { 300.0f / 80.f }; // aligned to 16-byte - auto sdr_multiplier = make_buffer(device.get(), sdr_multiplier_data); - if(!sdr_multiplier) { - BOOST_LOG(warning) << "Failed to create SDR multiplier"sv; + convert_Y_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertYPS.hlsl"); + if (!convert_Y_ps_hlsl) { return -1; } - device_ctx->PSSetConstantBuffers(0, 1, &sdr_multiplier); - } - else { - status = device->CreatePixelShader(scene_ps_hlsl->GetBufferPointer(), scene_ps_hlsl->GetBufferSize(), nullptr, &scene_ps); - if(status) { - BOOST_LOG(error) << "Failed to create scene pixel shader [0x"sv << util::hex(status).to_string_view() << ']'; + convert_Y_PQ_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertYPS_PQ.hlsl"); + if (!convert_Y_PQ_ps_hlsl) { return -1; } - } - blend_alpha = make_blend(device.get(), true, false); - blend_invert = make_blend(device.get(), true, true); - blend_disable = make_blend(device.get(), false, false); + convert_UV_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS.hlsl"); + if (!convert_UV_ps_hlsl) { + return -1; + } - if(!blend_disable || !blend_alpha || !blend_invert) { - return -1; - } + convert_UV_PQ_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS_PQ.hlsl"); + if (!convert_UV_PQ_ps_hlsl) { + return -1; + } - device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu); - device_ctx->PSSetSamplers(0, 1, &sampler_linear); - device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + convert_UV_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/ConvertUVVS.hlsl"); + if (!convert_UV_vs_hlsl) { + return -1; + } - return 0; -} + scene_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ScenePS.hlsl"); + if (!scene_ps_hlsl) { + return -1; + } -std::shared_ptr display_vram_t::alloc_img() { - auto img = std::make_shared(); + scene_NW_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ScenePS_NW.hlsl"); + if (!scene_NW_ps_hlsl) { + return -1; + } + BOOST_LOG(info) << "Compiled shaders"sv; - // Initialize format-independent fields - img->width = width; - img->height = height; - img->display = shared_from_this(); - img->id = next_image_id++; - - return img; -} - -// This cannot use ID3D11DeviceContext because it can be called concurrently by the encoding thread -int display_vram_t::complete_img(platf::img_t *img_base, bool dummy) { - auto img = (img_d3d_t *)img_base; - - // If this already has a capture texture and it's not switching dummy state, nothing to do - if(img->capture_texture && img->dummy == dummy) { return 0; } - - // If this is not a dummy image, we must know the format by now - if(!dummy && capture_format == DXGI_FORMAT_UNKNOWN) { - BOOST_LOG(error) << "display_vram_t::complete_img() called with unknown capture format!"; - return -1; - } - - // Reset the image (in case this was previously a dummy) - img->capture_texture.reset(); - img->capture_rt.reset(); - img->capture_mutex.reset(); - img->data = nullptr; - if(img->encoder_texture_handle) { - CloseHandle(img->encoder_texture_handle); - img->encoder_texture_handle = NULL; - } - - // Initialize format-dependent fields - img->pixel_pitch = get_pixel_pitch(); - img->row_pitch = img->pixel_pitch * img->width; - img->dummy = dummy; - - D3D11_TEXTURE2D_DESC t {}; - t.Width = img->width; - t.Height = img->height; - t.MipLevels = 1; - t.ArraySize = 1; - t.SampleDesc.Count = 1; - t.Usage = D3D11_USAGE_DEFAULT; - t.Format = (capture_format == DXGI_FORMAT_UNKNOWN) ? DXGI_FORMAT_B8G8R8A8_UNORM : capture_format; - t.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET; - t.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX; - - auto dummy_data = std::make_unique(img->row_pitch * img->height); - std::fill_n(dummy_data.get(), img->row_pitch * img->height, 0); - D3D11_SUBRESOURCE_DATA initial_data { - dummy_data.get(), - (UINT)img->row_pitch, - 0 - }; - - auto status = device->CreateTexture2D(&t, &initial_data, &img->capture_texture); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create img buf texture [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - status = device->CreateRenderTargetView(img->capture_texture.get(), nullptr, &img->capture_rt); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - // Get the keyed mutex to synchronize with the encoding code - status = img->capture_texture->QueryInterface(__uuidof(IDXGIKeyedMutex), (void **)&img->capture_mutex); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to query IDXGIKeyedMutex [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - resource1_t resource; - status = img->capture_texture->QueryInterface(__uuidof(IDXGIResource1), (void **)&resource); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to query IDXGIResource1 [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - // Create a handle for the encoder device to use to open this texture - status = resource->CreateSharedHandle(nullptr, DXGI_SHARED_RESOURCE_READ, nullptr, &img->encoder_texture_handle); - if(FAILED(status)) { - BOOST_LOG(error) << "Failed to create shared texture handle [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - - img->data = (std::uint8_t *)img->capture_texture.get(); - - return 0; -} - -// This cannot use ID3D11DeviceContext because it can be called concurrently by the encoding thread -int display_vram_t::dummy_img(platf::img_t *img_base) { - return complete_img(img_base, true); -} - -std::vector display_vram_t::get_supported_sdr_capture_formats() { - return { DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM }; -} - -std::vector display_vram_t::get_supported_hdr_capture_formats() { - return { - // scRGB FP16 is the desired format for HDR content. This will also handle - // 10-bit SDR displays with the increased precision of FP16 vs 8-bit UNORMs. - DXGI_FORMAT_R16G16B16A16_FLOAT, - - // DXGI_FORMAT_R10G10B10A2_UNORM seems like it might give us frames already - // converted to SMPTE 2084 PQ, however it seems to actually just clamp the - // scRGB FP16 values that DWM is using when the desktop format is scRGB FP16. - // - // If there is a case where the desktop format is really SMPTE 2084 PQ, it - // might make sense to support capturing it without conversion to scRGB, - // but we avoid it for now. - - // We include the 8-bit modes too for when the display is in SDR mode, - // while the client stream is HDR-capable. These UNORM formats behave - // like a degenerate case of scRGB FP16 with values between 0.0f-1.0f. - DXGI_FORMAT_B8G8R8A8_UNORM, - DXGI_FORMAT_R8G8B8A8_UNORM, - }; -} - -std::shared_ptr display_vram_t::make_hwdevice(pix_fmt_e pix_fmt) { - if(pix_fmt != platf::pix_fmt_e::nv12 && pix_fmt != platf::pix_fmt_e::p010) { - BOOST_LOG(error) << "display_vram_t doesn't support pixel format ["sv << from_pix_fmt(pix_fmt) << ']'; - - return nullptr; - } - - auto hwdevice = std::make_shared(); - - auto ret = hwdevice->init( - shared_from_this(), - adapter.get(), - pix_fmt); - - if(ret) { - return nullptr; - } - - return hwdevice; -} - -int init() { - BOOST_LOG(info) << "Compiling shaders..."sv; - scene_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/SceneVS.hlsl"); - if(!scene_vs_hlsl) { - return -1; - } - - convert_Y_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertYPS.hlsl"); - if(!convert_Y_ps_hlsl) { - return -1; - } - - convert_Y_PQ_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertYPS_PQ.hlsl"); - if(!convert_Y_PQ_ps_hlsl) { - return -1; - } - - convert_UV_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS.hlsl"); - if(!convert_UV_ps_hlsl) { - return -1; - } - - convert_UV_PQ_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS_PQ.hlsl"); - if(!convert_UV_PQ_ps_hlsl) { - return -1; - } - - convert_UV_vs_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR "/ConvertUVVS.hlsl"); - if(!convert_UV_vs_hlsl) { - return -1; - } - - scene_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ScenePS.hlsl"); - if(!scene_ps_hlsl) { - return -1; - } - - scene_NW_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ScenePS_NW.hlsl"); - if(!scene_NW_ps_hlsl) { - return -1; - } - BOOST_LOG(info) << "Compiled shaders"sv; - - return 0; -} -} // namespace platf::dxgi +} // namespace platf::dxgi diff --git a/src/platform/windows/input.cpp b/src/platform/windows/input.cpp index bd6265e4..0e537665 100644 --- a/src/platform/windows/input.cpp +++ b/src/platform/windows/input.cpp @@ -10,464 +10,485 @@ #include "src/platform/common.h" namespace platf { -using namespace std::literals; + using namespace std::literals; -thread_local HDESK _lastKnownInputDesktop = nullptr; + thread_local HDESK _lastKnownInputDesktop = nullptr; -constexpr touch_port_t target_touch_port { - 0, 0, - 65535, 65535 -}; + constexpr touch_port_t target_touch_port { + 0, 0, + 65535, 65535 + }; -using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>; -using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>; + using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>; + using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>; -static VIGEM_TARGET_TYPE map(const std::string_view &gp) { - if(gp == "x360"sv) { - return Xbox360Wired; + static VIGEM_TARGET_TYPE + map(const std::string_view &gp) { + if (gp == "x360"sv) { + return Xbox360Wired; + } + + return DualShock4Wired; } - return DualShock4Wired; -} + void CALLBACK + x360_notify( + client_t::pointer client, + target_t::pointer target, + std::uint8_t largeMotor, std::uint8_t smallMotor, + std::uint8_t /* led_number */, + void *userdata); -void CALLBACK x360_notify( - client_t::pointer client, - target_t::pointer target, - std::uint8_t largeMotor, std::uint8_t smallMotor, - std::uint8_t /* led_number */, - void *userdata); + void CALLBACK + ds4_notify( + client_t::pointer client, + target_t::pointer target, + std::uint8_t largeMotor, std::uint8_t smallMotor, + DS4_LIGHTBAR_COLOR /* led_color */, + void *userdata); -void CALLBACK ds4_notify( - client_t::pointer client, - target_t::pointer target, - std::uint8_t largeMotor, std::uint8_t smallMotor, - DS4_LIGHTBAR_COLOR /* led_color */, - void *userdata); + class vigem_t { + public: + int + init() { + VIGEM_ERROR status; -class vigem_t { -public: - int init() { - VIGEM_ERROR status; + client.reset(vigem_alloc()); - client.reset(vigem_alloc()); + status = vigem_connect(client.get()); + if (!VIGEM_SUCCESS(status)) { + BOOST_LOG(warning) << "Couldn't setup connection to ViGEm for gamepad support ["sv << util::hex(status).to_string_view() << ']'; - status = vigem_connect(client.get()); - if(!VIGEM_SUCCESS(status)) { - BOOST_LOG(warning) << "Couldn't setup connection to ViGEm for gamepad support ["sv << util::hex(status).to_string_view() << ']'; - - return -1; - } - - gamepads.resize(MAX_GAMEPADS); - - return 0; - } - - int alloc_gamepad_interal(int nr, rumble_queue_t &rumble_queue, VIGEM_TARGET_TYPE gp_type) { - auto &[rumble, gp] = gamepads[nr]; - assert(!gp); - - if(gp_type == Xbox360Wired) { - gp.reset(vigem_target_x360_alloc()); - } - else { - gp.reset(vigem_target_ds4_alloc()); - } - - auto status = vigem_target_add(client.get(), gp.get()); - if(!VIGEM_SUCCESS(status)) { - BOOST_LOG(error) << "Couldn't add Gamepad to ViGEm connection ["sv << util::hex(status).to_string_view() << ']'; - - return -1; - } - - rumble = std::move(rumble_queue); - - if(gp_type == Xbox360Wired) { - status = vigem_target_x360_register_notification(client.get(), gp.get(), x360_notify, this); - } - else { - status = vigem_target_ds4_register_notification(client.get(), gp.get(), ds4_notify, this); - } - - if(!VIGEM_SUCCESS(status)) { - BOOST_LOG(warning) << "Couldn't register notifications for rumble support ["sv << util::hex(status).to_string_view() << ']'; - } - - return 0; - } - - void free_target(int nr) { - auto &[_, gp] = gamepads[nr]; - - if(gp && vigem_target_is_attached(gp.get())) { - auto status = vigem_target_remove(client.get(), gp.get()); - if(!VIGEM_SUCCESS(status)) { - BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']'; + return -1; } + + gamepads.resize(MAX_GAMEPADS); + + return 0; } - gp.reset(); - } + int + alloc_gamepad_interal(int nr, rumble_queue_t &rumble_queue, VIGEM_TARGET_TYPE gp_type) { + auto &[rumble, gp] = gamepads[nr]; + assert(!gp); - void rumble(target_t::pointer target, std::uint8_t smallMotor, std::uint8_t largeMotor) { - for(int x = 0; x < gamepads.size(); ++x) { - auto &[rumble_queue, gp] = gamepads[x]; - - if(gp.get() == target) { - rumble_queue->raise(x, ((std::uint16_t)smallMotor) << 8, ((std::uint16_t)largeMotor) << 8); - - return; + if (gp_type == Xbox360Wired) { + gp.reset(vigem_target_x360_alloc()); + } + else { + gp.reset(vigem_target_ds4_alloc()); } - } - } - ~vigem_t() { - if(client) { - for(auto &[_, gp] : gamepads) { - if(gp && vigem_target_is_attached(gp.get())) { - auto status = vigem_target_remove(client.get(), gp.get()); - if(!VIGEM_SUCCESS(status)) { - BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']'; - } + auto status = vigem_target_add(client.get(), gp.get()); + if (!VIGEM_SUCCESS(status)) { + BOOST_LOG(error) << "Couldn't add Gamepad to ViGEm connection ["sv << util::hex(status).to_string_view() << ']'; + + return -1; + } + + rumble = std::move(rumble_queue); + + if (gp_type == Xbox360Wired) { + status = vigem_target_x360_register_notification(client.get(), gp.get(), x360_notify, this); + } + else { + status = vigem_target_ds4_register_notification(client.get(), gp.get(), ds4_notify, this); + } + + if (!VIGEM_SUCCESS(status)) { + BOOST_LOG(warning) << "Couldn't register notifications for rumble support ["sv << util::hex(status).to_string_view() << ']'; + } + + return 0; + } + + void + free_target(int nr) { + auto &[_, gp] = gamepads[nr]; + + if (gp && vigem_target_is_attached(gp.get())) { + auto status = vigem_target_remove(client.get(), gp.get()); + if (!VIGEM_SUCCESS(status)) { + BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']'; } } - vigem_disconnect(client.get()); + gp.reset(); + } + + void + rumble(target_t::pointer target, std::uint8_t smallMotor, std::uint8_t largeMotor) { + for (int x = 0; x < gamepads.size(); ++x) { + auto &[rumble_queue, gp] = gamepads[x]; + + if (gp.get() == target) { + rumble_queue->raise(x, ((std::uint16_t) smallMotor) << 8, ((std::uint16_t) largeMotor) << 8); + + return; + } + } + } + + ~vigem_t() { + if (client) { + for (auto &[_, gp] : gamepads) { + if (gp && vigem_target_is_attached(gp.get())) { + auto status = vigem_target_remove(client.get(), gp.get()); + if (!VIGEM_SUCCESS(status)) { + BOOST_LOG(warning) << "Couldn't detach gamepad from ViGEm ["sv << util::hex(status).to_string_view() << ']'; + } + } + } + + vigem_disconnect(client.get()); + } + } + + std::vector> gamepads; + + client_t client; + }; + + void CALLBACK + x360_notify( + client_t::pointer client, + target_t::pointer target, + std::uint8_t largeMotor, std::uint8_t smallMotor, + std::uint8_t /* led_number */, + void *userdata) { + BOOST_LOG(debug) + << "largeMotor: "sv << (int) largeMotor << std::endl + << "smallMotor: "sv << (int) smallMotor; + + task_pool.push(&vigem_t::rumble, (vigem_t *) userdata, target, smallMotor, largeMotor); + } + + void CALLBACK + ds4_notify( + client_t::pointer client, + target_t::pointer target, + std::uint8_t largeMotor, std::uint8_t smallMotor, + DS4_LIGHTBAR_COLOR /* led_color */, + void *userdata) { + BOOST_LOG(debug) + << "largeMotor: "sv << (int) largeMotor << std::endl + << "smallMotor: "sv << (int) smallMotor; + + task_pool.push(&vigem_t::rumble, (vigem_t *) userdata, target, smallMotor, largeMotor); + } + + struct input_raw_t { + ~input_raw_t() { + delete vigem; + } + + vigem_t *vigem; + HKL keyboard_layout; + HKL active_layout; + }; + + input_t + input() { + input_t result { new input_raw_t {} }; + auto &raw = *(input_raw_t *) result.get(); + + raw.vigem = new vigem_t {}; + if (raw.vigem->init()) { + delete raw.vigem; + raw.vigem = nullptr; + } + + // Moonlight currently sends keys normalized to the US English layout. + // We need to use that layout when converting to scancodes. + raw.keyboard_layout = LoadKeyboardLayoutA("00000409", 0); + if (!raw.keyboard_layout || LOWORD(raw.keyboard_layout) != 0x409) { + BOOST_LOG(warning) << "Unable to load US English keyboard layout for scancode translation. Keyboard input may not work in games."sv; + raw.keyboard_layout = NULL; + } + + // Activate layout for current process only + raw.active_layout = ActivateKeyboardLayout(raw.keyboard_layout, KLF_SETFORPROCESS); + if (!raw.active_layout) { + BOOST_LOG(warning) << "Unable to activate US English keyboard layout for scancode translation. Keyboard input may not work in games."sv; + raw.keyboard_layout = NULL; + } + + return result; + } + + void + send_input(INPUT &i) { + retry: + auto send = SendInput(1, &i, sizeof(INPUT)); + if (send != 1) { + auto hDesk = syncThreadDesktop(); + if (_lastKnownInputDesktop != hDesk) { + _lastKnownInputDesktop = hDesk; + goto retry; + } + BOOST_LOG(error) << "Couldn't send input"sv; } } - std::vector> gamepads; + void + abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { + INPUT i {}; - client_t client; -}; + i.type = INPUT_MOUSE; + auto &mi = i.mi; -void CALLBACK x360_notify( - client_t::pointer client, - target_t::pointer target, - std::uint8_t largeMotor, std::uint8_t smallMotor, - std::uint8_t /* led_number */, - void *userdata) { + mi.dwFlags = + MOUSEEVENTF_MOVE | + MOUSEEVENTF_ABSOLUTE | - BOOST_LOG(debug) - << "largeMotor: "sv << (int)largeMotor << std::endl - << "smallMotor: "sv << (int)smallMotor; + // MOUSEEVENTF_VIRTUALDESK maps to the entirety of the desktop rather than the primary desktop + MOUSEEVENTF_VIRTUALDESK; - task_pool.push(&vigem_t::rumble, (vigem_t *)userdata, target, smallMotor, largeMotor); -} + auto scaled_x = std::lround((x + touch_port.offset_x) * ((float) target_touch_port.width / (float) touch_port.width)); + auto scaled_y = std::lround((y + touch_port.offset_y) * ((float) target_touch_port.height / (float) touch_port.height)); -void CALLBACK ds4_notify( - client_t::pointer client, - target_t::pointer target, - std::uint8_t largeMotor, std::uint8_t smallMotor, - DS4_LIGHTBAR_COLOR /* led_color */, - void *userdata) { + mi.dx = scaled_x; + mi.dy = scaled_y; - BOOST_LOG(debug) - << "largeMotor: "sv << (int)largeMotor << std::endl - << "smallMotor: "sv << (int)smallMotor; - - task_pool.push(&vigem_t::rumble, (vigem_t *)userdata, target, smallMotor, largeMotor); -} - -struct input_raw_t { - ~input_raw_t() { - delete vigem; + send_input(i); } - vigem_t *vigem; - HKL keyboard_layout; - HKL active_layout; -}; + void + move_mouse(input_t &input, int deltaX, int deltaY) { + INPUT i {}; -input_t input() { - input_t result { new input_raw_t {} }; - auto &raw = *(input_raw_t *)result.get(); + i.type = INPUT_MOUSE; + auto &mi = i.mi; - raw.vigem = new vigem_t {}; - if(raw.vigem->init()) { - delete raw.vigem; - raw.vigem = nullptr; + mi.dwFlags = MOUSEEVENTF_MOVE; + mi.dx = deltaX; + mi.dy = deltaY; + + send_input(i); } - // Moonlight currently sends keys normalized to the US English layout. - // We need to use that layout when converting to scancodes. - raw.keyboard_layout = LoadKeyboardLayoutA("00000409", 0); - if(!raw.keyboard_layout || LOWORD(raw.keyboard_layout) != 0x409) { - BOOST_LOG(warning) << "Unable to load US English keyboard layout for scancode translation. Keyboard input may not work in games."sv; - raw.keyboard_layout = NULL; - } + void + button_mouse(input_t &input, int button, bool release) { + constexpr auto KEY_STATE_DOWN = (SHORT) 0x8000; - // Activate layout for current process only - raw.active_layout = ActivateKeyboardLayout(raw.keyboard_layout, KLF_SETFORPROCESS); - if(!raw.active_layout) { - BOOST_LOG(warning) << "Unable to activate US English keyboard layout for scancode translation. Keyboard input may not work in games."sv; - raw.keyboard_layout = NULL; - } + INPUT i {}; - return result; -} + i.type = INPUT_MOUSE; + auto &mi = i.mi; -void send_input(INPUT &i) { -retry: - auto send = SendInput(1, &i, sizeof(INPUT)); - if(send != 1) { - auto hDesk = syncThreadDesktop(); - if(_lastKnownInputDesktop != hDesk) { - _lastKnownInputDesktop = hDesk; - goto retry; + int mouse_button; + if (button == 1) { + mi.dwFlags = release ? MOUSEEVENTF_LEFTUP : MOUSEEVENTF_LEFTDOWN; + mouse_button = VK_LBUTTON; } - BOOST_LOG(error) << "Couldn't send input"sv; - } -} - -void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) { - INPUT i {}; - - i.type = INPUT_MOUSE; - auto &mi = i.mi; - - mi.dwFlags = - MOUSEEVENTF_MOVE | - MOUSEEVENTF_ABSOLUTE | - - // MOUSEEVENTF_VIRTUALDESK maps to the entirety of the desktop rather than the primary desktop - MOUSEEVENTF_VIRTUALDESK; - - auto scaled_x = std::lround((x + touch_port.offset_x) * ((float)target_touch_port.width / (float)touch_port.width)); - auto scaled_y = std::lround((y + touch_port.offset_y) * ((float)target_touch_port.height / (float)touch_port.height)); - - mi.dx = scaled_x; - mi.dy = scaled_y; - - send_input(i); -} - -void move_mouse(input_t &input, int deltaX, int deltaY) { - INPUT i {}; - - i.type = INPUT_MOUSE; - auto &mi = i.mi; - - mi.dwFlags = MOUSEEVENTF_MOVE; - mi.dx = deltaX; - mi.dy = deltaY; - - send_input(i); -} - -void button_mouse(input_t &input, int button, bool release) { - constexpr auto KEY_STATE_DOWN = (SHORT)0x8000; - - INPUT i {}; - - i.type = INPUT_MOUSE; - auto &mi = i.mi; - - int mouse_button; - if(button == 1) { - mi.dwFlags = release ? MOUSEEVENTF_LEFTUP : MOUSEEVENTF_LEFTDOWN; - mouse_button = VK_LBUTTON; - } - else if(button == 2) { - mi.dwFlags = release ? MOUSEEVENTF_MIDDLEUP : MOUSEEVENTF_MIDDLEDOWN; - mouse_button = VK_MBUTTON; - } - else if(button == 3) { - mi.dwFlags = release ? MOUSEEVENTF_RIGHTUP : MOUSEEVENTF_RIGHTDOWN; - mouse_button = VK_RBUTTON; - } - else if(button == 4) { - mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN; - mi.mouseData = XBUTTON1; - mouse_button = VK_XBUTTON1; - } - else { - mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN; - mi.mouseData = XBUTTON2; - mouse_button = VK_XBUTTON2; - } - - auto key_state = GetAsyncKeyState(mouse_button); - bool key_state_down = (key_state & KEY_STATE_DOWN) != 0; - if(key_state_down != release) { - BOOST_LOG(warning) << "Button state of mouse_button ["sv << button << "] does not match the desired state"sv; - - return; - } - - send_input(i); -} - -void scroll(input_t &input, int distance) { - INPUT i {}; - - i.type = INPUT_MOUSE; - auto &mi = i.mi; - - mi.dwFlags = MOUSEEVENTF_WHEEL; - mi.mouseData = distance; - - send_input(i); -} - -void hscroll(input_t &input, int distance) { - INPUT i {}; - - i.type = INPUT_MOUSE; - auto &mi = i.mi; - - mi.dwFlags = MOUSEEVENTF_HWHEEL; - mi.mouseData = distance; - - send_input(i); -} - -void keyboard(input_t &input, uint16_t modcode, bool release) { - auto raw = (input_raw_t *)input.get(); - - INPUT i {}; - i.type = INPUT_KEYBOARD; - auto &ki = i.ki; - - // For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/ - if(modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE && raw->keyboard_layout != NULL) { - ki.wScan = MapVirtualKeyEx(modcode, MAPVK_VK_TO_VSC, raw->keyboard_layout); - } - - // If we can map this to a scancode, send it as a scancode for maximum game compatibility. - if(ki.wScan) { - ki.dwFlags = KEYEVENTF_SCANCODE; - } - else { - // If there is no scancode mapping, send it as a regular VK event. - ki.wVk = modcode; - } - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#keystroke-message-flags - switch(modcode) { - case VK_RMENU: - case VK_RCONTROL: - case VK_INSERT: - case VK_DELETE: - case VK_HOME: - case VK_END: - case VK_PRIOR: - case VK_NEXT: - case VK_UP: - case VK_DOWN: - case VK_LEFT: - case VK_RIGHT: - case VK_DIVIDE: - ki.dwFlags |= KEYEVENTF_EXTENDEDKEY; - break; - default: - break; - } - - if(release) { - ki.dwFlags |= KEYEVENTF_KEYUP; - } - - send_input(i); -} - -void unicode(input_t &input, char *utf8, int size) { - // We can do no worse than one UTF-16 character per byte of UTF-8 - WCHAR wide[size]; - - int chars = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, size, wide, size); - if(chars <= 0) { - return; - } - - // Send all key down events - for(int i = 0; i < chars; i++) { - INPUT input {}; - input.type = INPUT_KEYBOARD; - input.ki.wScan = wide[i]; - input.ki.dwFlags = KEYEVENTF_UNICODE; - send_input(input); - } - - // Send all key up events - for(int i = 0; i < chars; i++) { - INPUT input {}; - input.type = INPUT_KEYBOARD; - input.ki.wScan = wide[i]; - input.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP; - send_input(input); - } -} - -int alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) { - auto raw = (input_raw_t *)input.get(); - - if(!raw->vigem) { - return 0; - } - - return raw->vigem->alloc_gamepad_interal(nr, rumble_queue, map(config::input.gamepad)); -} - -void free_gamepad(input_t &input, int nr) { - auto raw = (input_raw_t *)input.get(); - - if(!raw->vigem) { - return; - } - - raw->vigem->free_target(nr); -} - -static VIGEM_ERROR x360_update(client_t::pointer client, target_t::pointer gp, const gamepad_state_t &gamepad_state) { - auto &xusb = *(PXUSB_REPORT)&gamepad_state; - - return vigem_target_x360_update(client, gp, xusb); -} - -static DS4_DPAD_DIRECTIONS ds4_dpad(const gamepad_state_t &gamepad_state) { - auto flags = gamepad_state.buttonFlags; - if(flags & DPAD_UP) { - if(flags & DPAD_RIGHT) { - return DS4_BUTTON_DPAD_NORTHEAST; + else if (button == 2) { + mi.dwFlags = release ? MOUSEEVENTF_MIDDLEUP : MOUSEEVENTF_MIDDLEDOWN; + mouse_button = VK_MBUTTON; } - else if(flags & DPAD_LEFT) { - return DS4_BUTTON_DPAD_NORTHWEST; + else if (button == 3) { + mi.dwFlags = release ? MOUSEEVENTF_RIGHTUP : MOUSEEVENTF_RIGHTDOWN; + mouse_button = VK_RBUTTON; + } + else if (button == 4) { + mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN; + mi.mouseData = XBUTTON1; + mouse_button = VK_XBUTTON1; } else { - return DS4_BUTTON_DPAD_NORTH; + mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN; + mi.mouseData = XBUTTON2; + mouse_button = VK_XBUTTON2; } + + auto key_state = GetAsyncKeyState(mouse_button); + bool key_state_down = (key_state & KEY_STATE_DOWN) != 0; + if (key_state_down != release) { + BOOST_LOG(warning) << "Button state of mouse_button ["sv << button << "] does not match the desired state"sv; + + return; + } + + send_input(i); } - else if(flags & DPAD_DOWN) { - if(flags & DPAD_RIGHT) { - return DS4_BUTTON_DPAD_SOUTHEAST; + void + scroll(input_t &input, int distance) { + INPUT i {}; + + i.type = INPUT_MOUSE; + auto &mi = i.mi; + + mi.dwFlags = MOUSEEVENTF_WHEEL; + mi.mouseData = distance; + + send_input(i); + } + + void + hscroll(input_t &input, int distance) { + INPUT i {}; + + i.type = INPUT_MOUSE; + auto &mi = i.mi; + + mi.dwFlags = MOUSEEVENTF_HWHEEL; + mi.mouseData = distance; + + send_input(i); + } + + void + keyboard(input_t &input, uint16_t modcode, bool release) { + auto raw = (input_raw_t *) input.get(); + + INPUT i {}; + i.type = INPUT_KEYBOARD; + auto &ki = i.ki; + + // For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/ + if (modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE && raw->keyboard_layout != NULL) { + ki.wScan = MapVirtualKeyEx(modcode, MAPVK_VK_TO_VSC, raw->keyboard_layout); } - else if(flags & DPAD_LEFT) { - return DS4_BUTTON_DPAD_SOUTHWEST; + + // If we can map this to a scancode, send it as a scancode for maximum game compatibility. + if (ki.wScan) { + ki.dwFlags = KEYEVENTF_SCANCODE; } else { - return DS4_BUTTON_DPAD_SOUTH; + // If there is no scancode mapping, send it as a regular VK event. + ki.wVk = modcode; + } + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#keystroke-message-flags + switch (modcode) { + case VK_RMENU: + case VK_RCONTROL: + case VK_INSERT: + case VK_DELETE: + case VK_HOME: + case VK_END: + case VK_PRIOR: + case VK_NEXT: + case VK_UP: + case VK_DOWN: + case VK_LEFT: + case VK_RIGHT: + case VK_DIVIDE: + ki.dwFlags |= KEYEVENTF_EXTENDEDKEY; + break; + default: + break; + } + + if (release) { + ki.dwFlags |= KEYEVENTF_KEYUP; + } + + send_input(i); + } + + void + unicode(input_t &input, char *utf8, int size) { + // We can do no worse than one UTF-16 character per byte of UTF-8 + WCHAR wide[size]; + + int chars = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, size, wide, size); + if (chars <= 0) { + return; + } + + // Send all key down events + for (int i = 0; i < chars; i++) { + INPUT input {}; + input.type = INPUT_KEYBOARD; + input.ki.wScan = wide[i]; + input.ki.dwFlags = KEYEVENTF_UNICODE; + send_input(input); + } + + // Send all key up events + for (int i = 0; i < chars; i++) { + INPUT input {}; + input.type = INPUT_KEYBOARD; + input.ki.wScan = wide[i]; + input.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP; + send_input(input); } } - else if(flags & DPAD_RIGHT) { - return DS4_BUTTON_DPAD_EAST; + int + alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) { + auto raw = (input_raw_t *) input.get(); + + if (!raw->vigem) { + return 0; + } + + return raw->vigem->alloc_gamepad_interal(nr, rumble_queue, map(config::input.gamepad)); } - else if(flags & DPAD_LEFT) { - return DS4_BUTTON_DPAD_WEST; + void + free_gamepad(input_t &input, int nr) { + auto raw = (input_raw_t *) input.get(); + + if (!raw->vigem) { + return; + } + + raw->vigem->free_target(nr); } - return DS4_BUTTON_DPAD_NONE; -} + static VIGEM_ERROR + x360_update(client_t::pointer client, target_t::pointer gp, const gamepad_state_t &gamepad_state) { + auto &xusb = *(PXUSB_REPORT) &gamepad_state; -static DS4_BUTTONS ds4_buttons(const gamepad_state_t &gamepad_state) { - int buttons {}; + return vigem_target_x360_update(client, gp, xusb); + } - auto flags = gamepad_state.buttonFlags; - // clang-format off + static DS4_DPAD_DIRECTIONS + ds4_dpad(const gamepad_state_t &gamepad_state) { + auto flags = gamepad_state.buttonFlags; + if (flags & DPAD_UP) { + if (flags & DPAD_RIGHT) { + return DS4_BUTTON_DPAD_NORTHEAST; + } + else if (flags & DPAD_LEFT) { + return DS4_BUTTON_DPAD_NORTHWEST; + } + else { + return DS4_BUTTON_DPAD_NORTH; + } + } + + else if (flags & DPAD_DOWN) { + if (flags & DPAD_RIGHT) { + return DS4_BUTTON_DPAD_SOUTHEAST; + } + else if (flags & DPAD_LEFT) { + return DS4_BUTTON_DPAD_SOUTHWEST; + } + else { + return DS4_BUTTON_DPAD_SOUTH; + } + } + + else if (flags & DPAD_RIGHT) { + return DS4_BUTTON_DPAD_EAST; + } + + else if (flags & DPAD_LEFT) { + return DS4_BUTTON_DPAD_WEST; + } + + return DS4_BUTTON_DPAD_NONE; + } + + static DS4_BUTTONS + ds4_buttons(const gamepad_state_t &gamepad_state) { + int buttons {}; + + auto flags = gamepad_state.buttonFlags; + // clang-format off if(flags & LEFT_STICK) buttons |= DS4_BUTTON_THUMB_LEFT; if(flags & RIGHT_STICK) buttons |= DS4_BUTTON_THUMB_RIGHT; if(flags & LEFT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_LEFT; @@ -480,87 +501,93 @@ static DS4_BUTTONS ds4_buttons(const gamepad_state_t &gamepad_state) { if(gamepad_state.lt > 0) buttons |= DS4_BUTTON_TRIGGER_LEFT; if(gamepad_state.rt > 0) buttons |= DS4_BUTTON_TRIGGER_RIGHT; - // clang-format on + // clang-format on - return (DS4_BUTTONS)buttons; -} - -static DS4_SPECIAL_BUTTONS ds4_special_buttons(const gamepad_state_t &gamepad_state) { - int buttons {}; - - if(gamepad_state.buttonFlags & BACK) buttons |= DS4_SPECIAL_BUTTON_TOUCHPAD; - if(gamepad_state.buttonFlags & HOME) buttons |= DS4_SPECIAL_BUTTON_PS; - - return (DS4_SPECIAL_BUTTONS)buttons; -} - -static std::uint8_t to_ds4_triggerX(std::int16_t v) { - return (v + std::numeric_limits::max() / 2 + 1) / 257; -} - -static std::uint8_t to_ds4_triggerY(std::int16_t v) { - auto new_v = -((std::numeric_limits::max() / 2 + v - 1)) / 257; - - return new_v == 0 ? 0xFF : (std::uint8_t)new_v; -} - -static VIGEM_ERROR ds4_update(client_t::pointer client, target_t::pointer gp, const gamepad_state_t &gamepad_state) { - DS4_REPORT report; - - DS4_REPORT_INIT(&report); - DS4_SET_DPAD(&report, ds4_dpad(gamepad_state)); - report.wButtons |= ds4_buttons(gamepad_state); - report.bSpecial = ds4_special_buttons(gamepad_state); - - report.bTriggerL = gamepad_state.lt; - report.bTriggerR = gamepad_state.rt; - - report.bThumbLX = to_ds4_triggerX(gamepad_state.lsX); - report.bThumbLY = to_ds4_triggerY(gamepad_state.lsY); - - report.bThumbRX = to_ds4_triggerX(gamepad_state.rsX); - report.bThumbRY = to_ds4_triggerY(gamepad_state.rsY); - - return vigem_target_ds4_update(client, gp, report); -} - - -void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) { - auto vigem = ((input_raw_t *)input.get())->vigem; - - // If there is no gamepad support - if(!vigem) { - return; + return (DS4_BUTTONS) buttons; } - auto &[_, gp] = vigem->gamepads[nr]; + static DS4_SPECIAL_BUTTONS + ds4_special_buttons(const gamepad_state_t &gamepad_state) { + int buttons {}; - VIGEM_ERROR status; + if (gamepad_state.buttonFlags & BACK) buttons |= DS4_SPECIAL_BUTTON_TOUCHPAD; + if (gamepad_state.buttonFlags & HOME) buttons |= DS4_SPECIAL_BUTTON_PS; - if(vigem_target_get_type(gp.get()) == Xbox360Wired) { - status = x360_update(vigem->client.get(), gp.get(), gamepad_state); - } - else { - status = ds4_update(vigem->client.get(), gp.get(), gamepad_state); + return (DS4_SPECIAL_BUTTONS) buttons; } - if(!VIGEM_SUCCESS(status)) { - BOOST_LOG(warning) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']'; + static std::uint8_t + to_ds4_triggerX(std::int16_t v) { + return (v + std::numeric_limits::max() / 2 + 1) / 257; } -} -void freeInput(void *p) { - auto input = (input_raw_t *)p; + static std::uint8_t + to_ds4_triggerY(std::int16_t v) { + auto new_v = -((std::numeric_limits::max() / 2 + v - 1)) / 257; - delete input; -} + return new_v == 0 ? 0xFF : (std::uint8_t) new_v; + } -std::vector &supported_gamepads() { - // ds4 == ps4 - static std::vector gps { - "x360"sv, "ds4"sv, "ps4"sv - }; + static VIGEM_ERROR + ds4_update(client_t::pointer client, target_t::pointer gp, const gamepad_state_t &gamepad_state) { + DS4_REPORT report; - return gps; -} -} // namespace platf + DS4_REPORT_INIT(&report); + DS4_SET_DPAD(&report, ds4_dpad(gamepad_state)); + report.wButtons |= ds4_buttons(gamepad_state); + report.bSpecial = ds4_special_buttons(gamepad_state); + + report.bTriggerL = gamepad_state.lt; + report.bTriggerR = gamepad_state.rt; + + report.bThumbLX = to_ds4_triggerX(gamepad_state.lsX); + report.bThumbLY = to_ds4_triggerY(gamepad_state.lsY); + + report.bThumbRX = to_ds4_triggerX(gamepad_state.rsX); + report.bThumbRY = to_ds4_triggerY(gamepad_state.rsY); + + return vigem_target_ds4_update(client, gp, report); + } + + void + gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) { + auto vigem = ((input_raw_t *) input.get())->vigem; + + // If there is no gamepad support + if (!vigem) { + return; + } + + auto &[_, gp] = vigem->gamepads[nr]; + + VIGEM_ERROR status; + + if (vigem_target_get_type(gp.get()) == Xbox360Wired) { + status = x360_update(vigem->client.get(), gp.get(), gamepad_state); + } + else { + status = ds4_update(vigem->client.get(), gp.get(), gamepad_state); + } + + if (!VIGEM_SUCCESS(status)) { + BOOST_LOG(warning) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']'; + } + } + + void + freeInput(void *p) { + auto input = (input_raw_t *) p; + + delete input; + } + + std::vector & + supported_gamepads() { + // ds4 == ps4 + static std::vector gps { + "x360"sv, "ds4"sv, "ps4"sv + }; + + return gps; + } +} // namespace platf diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index 49f7ed0d..45b25e3d 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -28,7 +28,7 @@ // UDP_SEND_MSG_SIZE was added in the Windows 10 20H1 SDK #ifndef UDP_SEND_MSG_SIZE -#define UDP_SEND_MSG_SIZE 2 + #define UDP_SEND_MSG_SIZE 2 #endif // MinGW headers are missing qWAVE stuff @@ -37,838 +37,867 @@ typedef UINT32 QOS_FLOWID, *PQOS_FLOWID; #include #ifndef WLAN_API_MAKE_VERSION -#define WLAN_API_MAKE_VERSION(_major, _minor) (((DWORD)(_minor)) << 16 | (_major)) + #define WLAN_API_MAKE_VERSION(_major, _minor) (((DWORD) (_minor)) << 16 | (_major)) #endif namespace bp = boost::process; using namespace std::literals; namespace platf { -using adapteraddrs_t = util::c_ptr; + using adapteraddrs_t = util::c_ptr; -bool enabled_mouse_keys = false; -MOUSEKEYS previous_mouse_keys_state; + bool enabled_mouse_keys = false; + MOUSEKEYS previous_mouse_keys_state; -HANDLE qos_handle = nullptr; + HANDLE qos_handle = nullptr; -decltype(QOSCreateHandle) *fn_QOSCreateHandle = nullptr; -decltype(QOSAddSocketToFlow) *fn_QOSAddSocketToFlow = nullptr; -decltype(QOSRemoveSocketFromFlow) *fn_QOSRemoveSocketFromFlow = nullptr; + decltype(QOSCreateHandle) *fn_QOSCreateHandle = nullptr; + decltype(QOSAddSocketToFlow) *fn_QOSAddSocketToFlow = nullptr; + decltype(QOSRemoveSocketFromFlow) *fn_QOSRemoveSocketFromFlow = nullptr; -HANDLE wlan_handle = nullptr; + HANDLE wlan_handle = nullptr; -decltype(WlanOpenHandle) *fn_WlanOpenHandle = nullptr; -decltype(WlanCloseHandle) *fn_WlanCloseHandle = nullptr; -decltype(WlanFreeMemory) *fn_WlanFreeMemory = nullptr; -decltype(WlanEnumInterfaces) *fn_WlanEnumInterfaces = nullptr; -decltype(WlanSetInterface) *fn_WlanSetInterface = nullptr; + decltype(WlanOpenHandle) *fn_WlanOpenHandle = nullptr; + decltype(WlanCloseHandle) *fn_WlanCloseHandle = nullptr; + decltype(WlanFreeMemory) *fn_WlanFreeMemory = nullptr; + decltype(WlanEnumInterfaces) *fn_WlanEnumInterfaces = nullptr; + decltype(WlanSetInterface) *fn_WlanSetInterface = nullptr; -std::filesystem::path appdata() { - WCHAR sunshine_path[MAX_PATH]; - GetModuleFileNameW(NULL, sunshine_path, _countof(sunshine_path)); - return std::filesystem::path { sunshine_path }.remove_filename() / L"config"sv; -} - -std::string from_sockaddr(const sockaddr *const socket_address) { - char data[INET6_ADDRSTRLEN]; - - auto family = socket_address->sa_family; - if(family == AF_INET6) { - inet_ntop(AF_INET6, &((sockaddr_in6 *)socket_address)->sin6_addr, data, INET6_ADDRSTRLEN); + std::filesystem::path + appdata() { + WCHAR sunshine_path[MAX_PATH]; + GetModuleFileNameW(NULL, sunshine_path, _countof(sunshine_path)); + return std::filesystem::path { sunshine_path }.remove_filename() / L"config"sv; } - if(family == AF_INET) { - inet_ntop(AF_INET, &((sockaddr_in *)socket_address)->sin_addr, data, INET_ADDRSTRLEN); + std::string + from_sockaddr(const sockaddr *const socket_address) { + char data[INET6_ADDRSTRLEN]; + + auto family = socket_address->sa_family; + if (family == AF_INET6) { + inet_ntop(AF_INET6, &((sockaddr_in6 *) socket_address)->sin6_addr, data, INET6_ADDRSTRLEN); + } + + if (family == AF_INET) { + inet_ntop(AF_INET, &((sockaddr_in *) socket_address)->sin_addr, data, INET_ADDRSTRLEN); + } + + return std::string { data }; } - return std::string { data }; -} + std::pair + from_sockaddr_ex(const sockaddr *const ip_addr) { + char data[INET6_ADDRSTRLEN]; -std::pair from_sockaddr_ex(const sockaddr *const ip_addr) { - char data[INET6_ADDRSTRLEN]; + auto family = ip_addr->sa_family; + std::uint16_t port; + if (family == AF_INET6) { + inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); + port = ((sockaddr_in6 *) ip_addr)->sin6_port; + } - auto family = ip_addr->sa_family; - std::uint16_t port; - if(family == AF_INET6) { - inet_ntop(AF_INET6, &((sockaddr_in6 *)ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); - port = ((sockaddr_in6 *)ip_addr)->sin6_port; + if (family == AF_INET) { + inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); + port = ((sockaddr_in *) ip_addr)->sin_port; + } + + return { port, std::string { data } }; } - if(family == AF_INET) { - inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data, INET_ADDRSTRLEN); - port = ((sockaddr_in *)ip_addr)->sin_port; + adapteraddrs_t + get_adapteraddrs() { + adapteraddrs_t info { nullptr }; + ULONG size = 0; + + while (GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, info.get(), &size) == ERROR_BUFFER_OVERFLOW) { + info.reset((PIP_ADAPTER_ADDRESSES) malloc(size)); + } + + return info; } - return { port, std::string { data } }; -} - -adapteraddrs_t get_adapteraddrs() { - adapteraddrs_t info { nullptr }; - ULONG size = 0; - - while(GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, info.get(), &size) == ERROR_BUFFER_OVERFLOW) { - info.reset((PIP_ADAPTER_ADDRESSES)malloc(size)); - } - - return info; -} - -std::string get_mac_address(const std::string_view &address) { - adapteraddrs_t info = get_adapteraddrs(); - for(auto adapter_pos = info.get(); adapter_pos != nullptr; adapter_pos = adapter_pos->Next) { - for(auto addr_pos = adapter_pos->FirstUnicastAddress; addr_pos != nullptr; addr_pos = addr_pos->Next) { - if(adapter_pos->PhysicalAddressLength != 0 && address == from_sockaddr(addr_pos->Address.lpSockaddr)) { - std::stringstream mac_addr; - mac_addr << std::hex; - for(int i = 0; i < adapter_pos->PhysicalAddressLength; i++) { - if(i > 0) { - mac_addr << ':'; + std::string + get_mac_address(const std::string_view &address) { + adapteraddrs_t info = get_adapteraddrs(); + for (auto adapter_pos = info.get(); adapter_pos != nullptr; adapter_pos = adapter_pos->Next) { + for (auto addr_pos = adapter_pos->FirstUnicastAddress; addr_pos != nullptr; addr_pos = addr_pos->Next) { + if (adapter_pos->PhysicalAddressLength != 0 && address == from_sockaddr(addr_pos->Address.lpSockaddr)) { + std::stringstream mac_addr; + mac_addr << std::hex; + for (int i = 0; i < adapter_pos->PhysicalAddressLength; i++) { + if (i > 0) { + mac_addr << ':'; + } + mac_addr << std::setw(2) << std::setfill('0') << (int) adapter_pos->PhysicalAddress[i]; } - mac_addr << std::setw(2) << std::setfill('0') << (int)adapter_pos->PhysicalAddress[i]; + return mac_addr.str(); } - return mac_addr.str(); } } - } - BOOST_LOG(warning) << "Unable to find MAC address for "sv << address; - return "00:00:00:00:00:00"s; -} - -HDESK syncThreadDesktop() { - auto hDesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, GENERIC_ALL); - if(!hDesk) { - auto err = GetLastError(); - BOOST_LOG(error) << "Failed to Open Input Desktop [0x"sv << util::hex(err).to_string_view() << ']'; - - return nullptr; + BOOST_LOG(warning) << "Unable to find MAC address for "sv << address; + return "00:00:00:00:00:00"s; } - if(!SetThreadDesktop(hDesk)) { - auto err = GetLastError(); - BOOST_LOG(error) << "Failed to sync desktop to thread [0x"sv << util::hex(err).to_string_view() << ']'; - } + HDESK + syncThreadDesktop() { + auto hDesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, GENERIC_ALL); + if (!hDesk) { + auto err = GetLastError(); + BOOST_LOG(error) << "Failed to Open Input Desktop [0x"sv << util::hex(err).to_string_view() << ']'; - CloseDesktop(hDesk); - - return hDesk; -} - -void print_status(const std::string_view &prefix, HRESULT status) { - char err_string[1024]; - - DWORD bytes = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - nullptr, - status, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - err_string, - sizeof(err_string), - nullptr); - - BOOST_LOG(error) << prefix << ": "sv << std::string_view { err_string, bytes }; -} - -std::wstring utf8_to_wide_string(const std::string &str) { - // Determine the size required for the destination string - int chars = MultiByteToWideChar(CP_UTF8, 0, str.data(), str.length(), NULL, 0); - - // Allocate it - wchar_t buffer[chars] = {}; - - // Do the conversion for real - chars = MultiByteToWideChar(CP_UTF8, 0, str.data(), str.length(), buffer, chars); - return std::wstring(buffer, chars); -} - -std::string wide_to_utf8_string(const std::wstring &str) { - // Determine the size required for the destination string - int bytes = WideCharToMultiByte(CP_UTF8, 0, str.data(), str.length(), NULL, 0, NULL, NULL); - - // Allocate it - char buffer[bytes] = {}; - - // Do the conversion for real - bytes = WideCharToMultiByte(CP_UTF8, 0, str.data(), str.length(), buffer, bytes, NULL, NULL); - return std::string(buffer, bytes); -} - -HANDLE duplicate_shell_token() { - // Get the shell window (will usually be owned by explorer.exe) - HWND shell_window = GetShellWindow(); - if(!shell_window) { - BOOST_LOG(error) << "No shell window found. Is explorer.exe running?"sv; - return NULL; - } - - // Open a handle to the explorer.exe process - DWORD shell_pid; - GetWindowThreadProcessId(shell_window, &shell_pid); - HANDLE shell_process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, shell_pid); - if(!shell_process) { - BOOST_LOG(error) << "Failed to open shell process: "sv << GetLastError(); - return NULL; - } - - // Open explorer's token to clone for process creation - HANDLE shell_token; - BOOL ret = OpenProcessToken(shell_process, TOKEN_DUPLICATE, &shell_token); - CloseHandle(shell_process); - if(!ret) { - BOOST_LOG(error) << "Failed to open shell process token: "sv << GetLastError(); - return NULL; - } - - // Duplicate the token to make it usable for process creation - HANDLE new_token; - ret = DuplicateTokenEx(shell_token, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &new_token); - CloseHandle(shell_token); - if(!ret) { - BOOST_LOG(error) << "Failed to duplicate shell process token: "sv << GetLastError(); - return NULL; - } - - return new_token; -} - -PTOKEN_USER get_token_user(HANDLE token) { - DWORD return_length; - if(GetTokenInformation(token, TokenUser, NULL, 0, &return_length) || GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to get token information size: "sv << winerr; - return nullptr; - } - - auto user = (PTOKEN_USER)HeapAlloc(GetProcessHeap(), 0, return_length); - if(!user) { - return nullptr; - } - - if(!GetTokenInformation(token, TokenUser, user, return_length, &return_length)) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to get token information: "sv << winerr; - HeapFree(GetProcessHeap(), 0, user); - return nullptr; - } - - return user; -} - -void free_token_user(PTOKEN_USER user) { - HeapFree(GetProcessHeap(), 0, user); -} - -bool is_token_same_user_as_process(HANDLE other_token) { - HANDLE process_token; - if(!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &process_token)) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to open process token: "sv << winerr; - return false; - } - - auto process_user = get_token_user(process_token); - CloseHandle(process_token); - if(!process_user) { - return false; - } - - auto token_user = get_token_user(other_token); - if(!token_user) { - free_token_user(process_user); - return false; - } - - bool ret = EqualSid(process_user->User.Sid, token_user->User.Sid); - - free_token_user(process_user); - free_token_user(token_user); - - return ret; -} - -bool merge_user_environment_block(bp::environment &env, HANDLE shell_token) { - // Get the target user's environment block - PVOID env_block; - if(!CreateEnvironmentBlock(&env_block, shell_token, FALSE)) { - return false; - } - - // Parse the environment block and populate env - for(auto c = (PWCHAR)env_block; *c != UNICODE_NULL; c += wcslen(c) + 1) { - // Environment variable entries end with a null-terminator, so std::wstring() will get an entire entry. - std::string env_tuple = wide_to_utf8_string(std::wstring { c }); - std::string env_name = env_tuple.substr(0, env_tuple.find('=')); - std::string env_val = env_tuple.substr(env_tuple.find('=') + 1); - - // Perform a case-insensitive search to see if this variable name already exists - auto itr = std::find_if(env.cbegin(), env.cend(), - [&](const auto &e) { return boost::iequals(e.get_name(), env_name); }); - if(itr != env.cend()) { - // Use this existing name if it is already present to ensure we merge properly - env_name = itr->get_name(); + return nullptr; } - // For the PATH variable, we will merge the values together - if(boost::iequals(env_name, "PATH")) { - env[env_name] = env_val + ";" + env[env_name].to_string(); + if (!SetThreadDesktop(hDesk)) { + auto err = GetLastError(); + BOOST_LOG(error) << "Failed to sync desktop to thread [0x"sv << util::hex(err).to_string_view() << ']'; } - else { - // Other variables will be superseded by those in the user's environment block - env[env_name] = env_val; + + CloseDesktop(hDesk); + + return hDesk; + } + + void + print_status(const std::string_view &prefix, HRESULT status) { + char err_string[1024]; + + DWORD bytes = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + status, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + err_string, + sizeof(err_string), + nullptr); + + BOOST_LOG(error) << prefix << ": "sv << std::string_view { err_string, bytes }; + } + + std::wstring + utf8_to_wide_string(const std::string &str) { + // Determine the size required for the destination string + int chars = MultiByteToWideChar(CP_UTF8, 0, str.data(), str.length(), NULL, 0); + + // Allocate it + wchar_t buffer[chars] = {}; + + // Do the conversion for real + chars = MultiByteToWideChar(CP_UTF8, 0, str.data(), str.length(), buffer, chars); + return std::wstring(buffer, chars); + } + + std::string + wide_to_utf8_string(const std::wstring &str) { + // Determine the size required for the destination string + int bytes = WideCharToMultiByte(CP_UTF8, 0, str.data(), str.length(), NULL, 0, NULL, NULL); + + // Allocate it + char buffer[bytes] = {}; + + // Do the conversion for real + bytes = WideCharToMultiByte(CP_UTF8, 0, str.data(), str.length(), buffer, bytes, NULL, NULL); + return std::string(buffer, bytes); + } + + HANDLE + duplicate_shell_token() { + // Get the shell window (will usually be owned by explorer.exe) + HWND shell_window = GetShellWindow(); + if (!shell_window) { + BOOST_LOG(error) << "No shell window found. Is explorer.exe running?"sv; + return NULL; } - } - DestroyEnvironmentBlock(env_block); - return true; -} + // Open a handle to the explorer.exe process + DWORD shell_pid; + GetWindowThreadProcessId(shell_window, &shell_pid); + HANDLE shell_process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, shell_pid); + if (!shell_process) { + BOOST_LOG(error) << "Failed to open shell process: "sv << GetLastError(); + return NULL; + } -// Note: This does NOT append a null terminator -void append_string_to_environment_block(wchar_t *env_block, int &offset, const std::wstring &wstr) { - std::memcpy(&env_block[offset], wstr.data(), wstr.length() * sizeof(wchar_t)); - offset += wstr.length(); -} + // Open explorer's token to clone for process creation + HANDLE shell_token; + BOOL ret = OpenProcessToken(shell_process, TOKEN_DUPLICATE, &shell_token); + CloseHandle(shell_process); + if (!ret) { + BOOST_LOG(error) << "Failed to open shell process token: "sv << GetLastError(); + return NULL; + } -std::wstring create_environment_block(bp::environment &env) { - int size = 0; - for(const auto &entry : env) { - auto name = entry.get_name(); - auto value = entry.to_string(); - size += utf8_to_wide_string(name).length() + 1 /* L'=' */ + utf8_to_wide_string(value).length() + 1 /* L'\0' */; - } - - size += 1 /* L'\0' */; - - wchar_t env_block[size]; - int offset = 0; - for(const auto &entry : env) { - auto name = entry.get_name(); - auto value = entry.to_string(); - - // Construct the NAME=VAL\0 string - append_string_to_environment_block(env_block, offset, utf8_to_wide_string(name)); - env_block[offset++] = L'='; - append_string_to_environment_block(env_block, offset, utf8_to_wide_string(value)); - env_block[offset++] = L'\0'; - } - - // Append a final null terminator - env_block[offset++] = L'\0'; - - return std::wstring(env_block, offset); -} - -LPPROC_THREAD_ATTRIBUTE_LIST allocate_proc_thread_attr_list(DWORD attribute_count) { - SIZE_T size; - InitializeProcThreadAttributeList(NULL, attribute_count, 0, &size); - - auto list = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, size); - if(list == NULL) { - return NULL; - } - - if(!InitializeProcThreadAttributeList(list, attribute_count, 0, &size)) { - HeapFree(GetProcessHeap(), 0, list); - return NULL; - } - - return list; -} - -void free_proc_thread_attr_list(LPPROC_THREAD_ATTRIBUTE_LIST list) { - DeleteProcThreadAttributeList(list); - HeapFree(GetProcessHeap(), 0, list); -} - -bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - HANDLE shell_token = duplicate_shell_token(); - if(!shell_token) { - // This can happen if the shell has crashed. Fail the launch rather than risking launching with - // Sunshine's permissions unmodified. - ec = std::make_error_code(std::errc::no_such_process); - return bp::child(); - } - - auto token_close = util::fail_guard([shell_token]() { + // Duplicate the token to make it usable for process creation + HANDLE new_token; + ret = DuplicateTokenEx(shell_token, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &new_token); CloseHandle(shell_token); - }); + if (!ret) { + BOOST_LOG(error) << "Failed to duplicate shell process token: "sv << GetLastError(); + return NULL; + } - // Populate env with user-specific environment variables - if(!merge_user_environment_block(env, shell_token)) { - ec = std::make_error_code(std::errc::not_enough_memory); - return bp::child(); + return new_token; } - // Most Win32 APIs can't consume UTF-8 strings directly, so we must convert them into UTF-16 - std::wstring wcmd = utf8_to_wide_string(cmd); - std::wstring env_block = create_environment_block(env); - std::wstring start_dir = utf8_to_wide_string(working_dir.string()); + PTOKEN_USER + get_token_user(HANDLE token) { + DWORD return_length; + if (GetTokenInformation(token, TokenUser, NULL, 0, &return_length) || GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to get token information size: "sv << winerr; + return nullptr; + } - STARTUPINFOEXW startup_info = {}; - startup_info.StartupInfo.cb = sizeof(startup_info); + auto user = (PTOKEN_USER) HeapAlloc(GetProcessHeap(), 0, return_length); + if (!user) { + return nullptr; + } - // Allocate a process attribute list with space for 1 element - startup_info.lpAttributeList = allocate_proc_thread_attr_list(1); - if(startup_info.lpAttributeList == NULL) { - ec = std::make_error_code(std::errc::not_enough_memory); - return bp::child(); + if (!GetTokenInformation(token, TokenUser, user, return_length, &return_length)) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to get token information: "sv << winerr; + HeapFree(GetProcessHeap(), 0, user); + return nullptr; + } + + return user; } - auto attr_list_free = util::fail_guard([list = startup_info.lpAttributeList]() { - free_proc_thread_attr_list(list); - }); - - if(file) { - HANDLE log_file_handle = (HANDLE)_get_osfhandle(_fileno(file)); - - // Populate std handles if the caller gave us a log file to use - startup_info.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; - startup_info.StartupInfo.hStdInput = NULL; - startup_info.StartupInfo.hStdOutput = log_file_handle; - startup_info.StartupInfo.hStdError = log_file_handle; - - // Allow the log file handle to be inherited by the child process (without inheriting all of - // our inheritable handles, such as our own log file handle created by SunshineSvc). - UpdateProcThreadAttribute(startup_info.lpAttributeList, - 0, - PROC_THREAD_ATTRIBUTE_HANDLE_LIST, - &log_file_handle, - sizeof(log_file_handle), - NULL, - NULL); + void + free_token_user(PTOKEN_USER user) { + HeapFree(GetProcessHeap(), 0, user); } - // If we're running with the same user account as the shell, just use CreateProcess(). - // This will launch the child process elevated if Sunshine is elevated. - PROCESS_INFORMATION process_info; - BOOL ret; - if(!is_token_same_user_as_process(shell_token)) { - // Impersonate the user when launching the process. This will ensure that appropriate access - // checks are done against the user token, not our SYSTEM token. It will also allow network - // shares and mapped network drives to be used as launch targets, since those credentials - // are stored per-user. - if(!ImpersonateLoggedOnUser(shell_token)) { - auto winerror = GetLastError(); - BOOST_LOG(error) << "Failed to impersonate user: "sv << winerror; - ec = std::make_error_code(std::errc::permission_denied); + bool + is_token_same_user_as_process(HANDLE other_token) { + HANDLE process_token; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &process_token)) { + auto winerr = GetLastError(); + BOOST_LOG(error) << "Failed to open process token: "sv << winerr; + return false; + } + + auto process_user = get_token_user(process_token); + CloseHandle(process_token); + if (!process_user) { + return false; + } + + auto token_user = get_token_user(other_token); + if (!token_user) { + free_token_user(process_user); + return false; + } + + bool ret = EqualSid(process_user->User.Sid, token_user->User.Sid); + + free_token_user(process_user); + free_token_user(token_user); + + return ret; + } + + bool + merge_user_environment_block(bp::environment &env, HANDLE shell_token) { + // Get the target user's environment block + PVOID env_block; + if (!CreateEnvironmentBlock(&env_block, shell_token, FALSE)) { + return false; + } + + // Parse the environment block and populate env + for (auto c = (PWCHAR) env_block; *c != UNICODE_NULL; c += wcslen(c) + 1) { + // Environment variable entries end with a null-terminator, so std::wstring() will get an entire entry. + std::string env_tuple = wide_to_utf8_string(std::wstring { c }); + std::string env_name = env_tuple.substr(0, env_tuple.find('=')); + std::string env_val = env_tuple.substr(env_tuple.find('=') + 1); + + // Perform a case-insensitive search to see if this variable name already exists + auto itr = std::find_if(env.cbegin(), env.cend(), + [&](const auto &e) { return boost::iequals(e.get_name(), env_name); }); + if (itr != env.cend()) { + // Use this existing name if it is already present to ensure we merge properly + env_name = itr->get_name(); + } + + // For the PATH variable, we will merge the values together + if (boost::iequals(env_name, "PATH")) { + env[env_name] = env_val + ";" + env[env_name].to_string(); + } + else { + // Other variables will be superseded by those in the user's environment block + env[env_name] = env_val; + } + } + + DestroyEnvironmentBlock(env_block); + return true; + } + + // Note: This does NOT append a null terminator + void + append_string_to_environment_block(wchar_t *env_block, int &offset, const std::wstring &wstr) { + std::memcpy(&env_block[offset], wstr.data(), wstr.length() * sizeof(wchar_t)); + offset += wstr.length(); + } + + std::wstring + create_environment_block(bp::environment &env) { + int size = 0; + for (const auto &entry : env) { + auto name = entry.get_name(); + auto value = entry.to_string(); + size += utf8_to_wide_string(name).length() + 1 /* L'=' */ + utf8_to_wide_string(value).length() + 1 /* L'\0' */; + } + + size += 1 /* L'\0' */; + + wchar_t env_block[size]; + int offset = 0; + for (const auto &entry : env) { + auto name = entry.get_name(); + auto value = entry.to_string(); + + // Construct the NAME=VAL\0 string + append_string_to_environment_block(env_block, offset, utf8_to_wide_string(name)); + env_block[offset++] = L'='; + append_string_to_environment_block(env_block, offset, utf8_to_wide_string(value)); + env_block[offset++] = L'\0'; + } + + // Append a final null terminator + env_block[offset++] = L'\0'; + + return std::wstring(env_block, offset); + } + + LPPROC_THREAD_ATTRIBUTE_LIST + allocate_proc_thread_attr_list(DWORD attribute_count) { + SIZE_T size; + InitializeProcThreadAttributeList(NULL, attribute_count, 0, &size); + + auto list = (LPPROC_THREAD_ATTRIBUTE_LIST) HeapAlloc(GetProcessHeap(), 0, size); + if (list == NULL) { + return NULL; + } + + if (!InitializeProcThreadAttributeList(list, attribute_count, 0, &size)) { + HeapFree(GetProcessHeap(), 0, list); + return NULL; + } + + return list; + } + + void + free_proc_thread_attr_list(LPPROC_THREAD_ATTRIBUTE_LIST list) { + DeleteProcThreadAttributeList(list); + HeapFree(GetProcessHeap(), 0, list); + } + + bp::child + run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { + HANDLE shell_token = duplicate_shell_token(); + if (!shell_token) { + // This can happen if the shell has crashed. Fail the launch rather than risking launching with + // Sunshine's permissions unmodified. + ec = std::make_error_code(std::errc::no_such_process); return bp::child(); } - // Launch the process with the duplicated shell token. - // Set CREATE_BREAKAWAY_FROM_JOB to avoid the child being killed if SunshineSvc.exe is terminated. - // Set CREATE_NEW_CONSOLE to avoid writing stdout to Sunshine's log if 'file' is not specified. - ret = CreateProcessAsUserW(shell_token, - NULL, - (LPWSTR)wcmd.c_str(), - NULL, - NULL, - !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), - EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB, - env_block.data(), - start_dir.empty() ? NULL : start_dir.c_str(), - (LPSTARTUPINFOW)&startup_info, - &process_info); + auto token_close = util::fail_guard([shell_token]() { + CloseHandle(shell_token); + }); - if(!ret) { - auto error = GetLastError(); + // Populate env with user-specific environment variables + if (!merge_user_environment_block(env, shell_token)) { + ec = std::make_error_code(std::errc::not_enough_memory); + return bp::child(); + } - if(error == 740) { - BOOST_LOG(info) << "Could not execute previous command because it required elevation. Running the command again with elevation, for security reasons this will prompt user interaction."sv; - startup_info.StartupInfo.wShowWindow = SW_HIDE; - startup_info.StartupInfo.dwFlags = startup_info.StartupInfo.dwFlags | STARTF_USESHOWWINDOW; - std::wstring elevated_command = L"tools\\elevator.exe "; - elevated_command += wcmd; + // Most Win32 APIs can't consume UTF-8 strings directly, so we must convert them into UTF-16 + std::wstring wcmd = utf8_to_wide_string(cmd); + std::wstring env_block = create_environment_block(env); + std::wstring start_dir = utf8_to_wide_string(working_dir.string()); - // For security reasons, Windows enforces that an application can have only one "interactive thread" responsible for processing user input and managing the user interface (UI). - // Since UAC prompts are interactive, we cannot have a UAC prompt while Sunshine is already running because it would block the thread. - // To work around this issue, we will launch a separate process that will elevate the command, which will prompt the user to confirm the elevation. - // This is our intended behavior: to require interaction before elevating the command. - ret = CreateProcessAsUserW(shell_token, - nullptr, - (LPWSTR)elevated_command.c_str(), - nullptr, - nullptr, - !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), - EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB, - env_block.data(), - start_dir.empty() ? nullptr : start_dir.c_str(), - (LPSTARTUPINFOW)&startup_info, - &process_info); + STARTUPINFOEXW startup_info = {}; + startup_info.StartupInfo.cb = sizeof(startup_info); + + // Allocate a process attribute list with space for 1 element + startup_info.lpAttributeList = allocate_proc_thread_attr_list(1); + if (startup_info.lpAttributeList == NULL) { + ec = std::make_error_code(std::errc::not_enough_memory); + return bp::child(); + } + + auto attr_list_free = util::fail_guard([list = startup_info.lpAttributeList]() { + free_proc_thread_attr_list(list); + }); + + if (file) { + HANDLE log_file_handle = (HANDLE) _get_osfhandle(_fileno(file)); + + // Populate std handles if the caller gave us a log file to use + startup_info.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + startup_info.StartupInfo.hStdInput = NULL; + startup_info.StartupInfo.hStdOutput = log_file_handle; + startup_info.StartupInfo.hStdError = log_file_handle; + + // Allow the log file handle to be inherited by the child process (without inheriting all of + // our inheritable handles, such as our own log file handle created by SunshineSvc). + UpdateProcThreadAttribute(startup_info.lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + &log_file_handle, + sizeof(log_file_handle), + NULL, + NULL); + } + + // If we're running with the same user account as the shell, just use CreateProcess(). + // This will launch the child process elevated if Sunshine is elevated. + PROCESS_INFORMATION process_info; + BOOL ret; + if (!is_token_same_user_as_process(shell_token)) { + // Impersonate the user when launching the process. This will ensure that appropriate access + // checks are done against the user token, not our SYSTEM token. It will also allow network + // shares and mapped network drives to be used as launch targets, since those credentials + // are stored per-user. + if (!ImpersonateLoggedOnUser(shell_token)) { + auto winerror = GetLastError(); + BOOST_LOG(error) << "Failed to impersonate user: "sv << winerror; + ec = std::make_error_code(std::errc::permission_denied); + return bp::child(); } - } - // End impersonation of the logged on user. If this fails (which is extremely unlikely), - // we will be running with an unknown user token. The only safe thing to do in that case - // is terminate ourselves. - if(!RevertToSelf()) { - auto winerror = GetLastError(); - BOOST_LOG(fatal) << "Failed to revert to self after impersonation: "sv << winerror; - std::abort(); - } - } - else { - ret = CreateProcessW(NULL, - (LPWSTR)wcmd.c_str(), - NULL, - NULL, - !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), - EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB, - env_block.data(), - start_dir.empty() ? NULL : start_dir.c_str(), - (LPSTARTUPINFOW)&startup_info, - &process_info); - } + // Launch the process with the duplicated shell token. + // Set CREATE_BREAKAWAY_FROM_JOB to avoid the child being killed if SunshineSvc.exe is terminated. + // Set CREATE_NEW_CONSOLE to avoid writing stdout to Sunshine's log if 'file' is not specified. + ret = CreateProcessAsUserW(shell_token, + NULL, + (LPWSTR) wcmd.c_str(), + NULL, + NULL, + !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), + EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB, + env_block.data(), + start_dir.empty() ? NULL : start_dir.c_str(), + (LPSTARTUPINFOW) &startup_info, + &process_info); - if(ret) { - // Since we are always spawning a process with a less privileged token than ourselves, - // bp::child() should have no problem opening it with any access rights it wants. - auto child = bp::child((bp::pid_t)process_info.dwProcessId); - if(group) { - group->add(child); - } + if (!ret) { + auto error = GetLastError(); - // Only close handles after bp::child() has opened the process. If the process terminates - // quickly, the PID could be reused if we close the process handle. - CloseHandle(process_info.hThread); - CloseHandle(process_info.hProcess); + if (error == 740) { + BOOST_LOG(info) << "Could not execute previous command because it required elevation. Running the command again with elevation, for security reasons this will prompt user interaction."sv; + startup_info.StartupInfo.wShowWindow = SW_HIDE; + startup_info.StartupInfo.dwFlags = startup_info.StartupInfo.dwFlags | STARTF_USESHOWWINDOW; + std::wstring elevated_command = L"tools\\elevator.exe "; + elevated_command += wcmd; - BOOST_LOG(info) << cmd << " running with PID "sv << child.id(); - return child; - } - else { - // We must NOT try bp::child() here, since this case can potentially be induced by ACL - // manipulation (denying yourself execute permission) to cause an escalation of privilege. - auto winerror = GetLastError(); - BOOST_LOG(error) << "Failed to launch process: "sv << winerror; - ec = std::make_error_code(std::errc::invalid_argument); - return bp::child(); - } -} - -void adjust_thread_priority(thread_priority_e priority) { - int win32_priority; - - switch(priority) { - case thread_priority_e::low: - win32_priority = THREAD_PRIORITY_BELOW_NORMAL; - break; - case thread_priority_e::normal: - win32_priority = THREAD_PRIORITY_NORMAL; - break; - case thread_priority_e::high: - win32_priority = THREAD_PRIORITY_ABOVE_NORMAL; - break; - case thread_priority_e::critical: - win32_priority = THREAD_PRIORITY_HIGHEST; - break; - default: - BOOST_LOG(error) << "Unknown thread priority: "sv << (int)priority; - return; - } - - if(!SetThreadPriority(GetCurrentThread(), win32_priority)) { - auto winerr = GetLastError(); - BOOST_LOG(warning) << "Unable to set thread priority to "sv << win32_priority << ": "sv << winerr; - } -} - -void streaming_will_start() { - static std::once_flag load_wlanapi_once_flag; - std::call_once(load_wlanapi_once_flag, []() { - // wlanapi.dll is not installed by default on Windows Server, so we load it dynamically - HMODULE wlanapi = LoadLibraryExA("wlanapi.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); - if(!wlanapi) { - BOOST_LOG(debug) << "wlanapi.dll is not available on this OS"sv; - return; - } - - fn_WlanOpenHandle = (decltype(fn_WlanOpenHandle))GetProcAddress(wlanapi, "WlanOpenHandle"); - fn_WlanCloseHandle = (decltype(fn_WlanCloseHandle))GetProcAddress(wlanapi, "WlanCloseHandle"); - fn_WlanFreeMemory = (decltype(fn_WlanFreeMemory))GetProcAddress(wlanapi, "WlanFreeMemory"); - fn_WlanEnumInterfaces = (decltype(fn_WlanEnumInterfaces))GetProcAddress(wlanapi, "WlanEnumInterfaces"); - fn_WlanSetInterface = (decltype(fn_WlanSetInterface))GetProcAddress(wlanapi, "WlanSetInterface"); - - if(!fn_WlanOpenHandle || !fn_WlanCloseHandle || !fn_WlanFreeMemory || !fn_WlanEnumInterfaces || !fn_WlanSetInterface) { - BOOST_LOG(error) << "wlanapi.dll is missing exports?"sv; - - fn_WlanOpenHandle = nullptr; - fn_WlanCloseHandle = nullptr; - fn_WlanFreeMemory = nullptr; - fn_WlanEnumInterfaces = nullptr; - fn_WlanSetInterface = nullptr; - - FreeLibrary(wlanapi); - return; - } - }); - - // Enable MMCSS scheduling for DWM - DwmEnableMMCSS(true); - - // Reduce timer period to 1ms - timeBeginPeriod(1); - - // Promote ourselves to high priority class - SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); - - // Enable low latency mode on all connected WLAN NICs if wlanapi.dll is available - if(fn_WlanOpenHandle) { - DWORD negotiated_version; - - if(fn_WlanOpenHandle(WLAN_API_MAKE_VERSION(2, 0), nullptr, &negotiated_version, &wlan_handle) == ERROR_SUCCESS) { - PWLAN_INTERFACE_INFO_LIST wlan_interface_list; - - if(fn_WlanEnumInterfaces(wlan_handle, nullptr, &wlan_interface_list) == ERROR_SUCCESS) { - for(DWORD i = 0; i < wlan_interface_list->dwNumberOfItems; i++) { - if(wlan_interface_list->InterfaceInfo[i].isState == wlan_interface_state_connected) { - // Enable media streaming mode for 802.11 wireless interfaces to reduce latency and - // unneccessary background scanning operations that cause packet loss and jitter. - // - // https://docs.microsoft.com/en-us/windows-hardware/drivers/network/oid-wdi-set-connection-quality - // https://docs.microsoft.com/en-us/previous-versions/windows/hardware/wireless/native-802-11-media-streaming - BOOL value = TRUE; - auto error = fn_WlanSetInterface(wlan_handle, &wlan_interface_list->InterfaceInfo[i].InterfaceGuid, - wlan_intf_opcode_media_streaming_mode, sizeof(value), &value, nullptr); - if(error == ERROR_SUCCESS) { - BOOST_LOG(info) << "WLAN interface "sv << i << " is now in low latency mode"sv; - } - } + // For security reasons, Windows enforces that an application can have only one "interactive thread" responsible for processing user input and managing the user interface (UI). + // Since UAC prompts are interactive, we cannot have a UAC prompt while Sunshine is already running because it would block the thread. + // To work around this issue, we will launch a separate process that will elevate the command, which will prompt the user to confirm the elevation. + // This is our intended behavior: to require interaction before elevating the command. + ret = CreateProcessAsUserW(shell_token, + nullptr, + (LPWSTR) elevated_command.c_str(), + nullptr, + nullptr, + !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), + EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB, + env_block.data(), + start_dir.empty() ? nullptr : start_dir.c_str(), + (LPSTARTUPINFOW) &startup_info, + &process_info); } - - fn_WlanFreeMemory(wlan_interface_list); } - else { - fn_WlanCloseHandle(wlan_handle, nullptr); - wlan_handle = NULL; - } - } - } - // If there is no mouse connected, enable Mouse Keys to force the cursor to appear - if(!GetSystemMetrics(SM_MOUSEPRESENT)) { - BOOST_LOG(info) << "A mouse was not detected. Sunshine will enable Mouse Keys while streaming to force the mouse cursor to appear."; - - // Get the current state of Mouse Keys so we can restore it when streaming is over - previous_mouse_keys_state.cbSize = sizeof(previous_mouse_keys_state); - if(SystemParametersInfoW(SPI_GETMOUSEKEYS, 0, &previous_mouse_keys_state, 0)) { - MOUSEKEYS new_mouse_keys_state = {}; - - // Enable Mouse Keys - new_mouse_keys_state.cbSize = sizeof(new_mouse_keys_state); - new_mouse_keys_state.dwFlags = MKF_MOUSEKEYSON | MKF_AVAILABLE; - new_mouse_keys_state.iMaxSpeed = 10; - new_mouse_keys_state.iTimeToMaxSpeed = 1000; - if(SystemParametersInfoW(SPI_SETMOUSEKEYS, 0, &new_mouse_keys_state, 0)) { - // Remember to restore the previous settings when we stop streaming - enabled_mouse_keys = true; - } - else { - auto winerr = GetLastError(); - BOOST_LOG(warning) << "Unable to enable Mouse Keys: "sv << winerr; + // End impersonation of the logged on user. If this fails (which is extremely unlikely), + // we will be running with an unknown user token. The only safe thing to do in that case + // is terminate ourselves. + if (!RevertToSelf()) { + auto winerror = GetLastError(); + BOOST_LOG(fatal) << "Failed to revert to self after impersonation: "sv << winerror; + std::abort(); } } else { + ret = CreateProcessW(NULL, + (LPWSTR) wcmd.c_str(), + NULL, + NULL, + !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), + EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB, + env_block.data(), + start_dir.empty() ? NULL : start_dir.c_str(), + (LPSTARTUPINFOW) &startup_info, + &process_info); + } + + if (ret) { + // Since we are always spawning a process with a less privileged token than ourselves, + // bp::child() should have no problem opening it with any access rights it wants. + auto child = bp::child((bp::pid_t) process_info.dwProcessId); + if (group) { + group->add(child); + } + + // Only close handles after bp::child() has opened the process. If the process terminates + // quickly, the PID could be reused if we close the process handle. + CloseHandle(process_info.hThread); + CloseHandle(process_info.hProcess); + + BOOST_LOG(info) << cmd << " running with PID "sv << child.id(); + return child; + } + else { + // We must NOT try bp::child() here, since this case can potentially be induced by ACL + // manipulation (denying yourself execute permission) to cause an escalation of privilege. + auto winerror = GetLastError(); + BOOST_LOG(error) << "Failed to launch process: "sv << winerror; + ec = std::make_error_code(std::errc::invalid_argument); + return bp::child(); + } + } + + void + adjust_thread_priority(thread_priority_e priority) { + int win32_priority; + + switch (priority) { + case thread_priority_e::low: + win32_priority = THREAD_PRIORITY_BELOW_NORMAL; + break; + case thread_priority_e::normal: + win32_priority = THREAD_PRIORITY_NORMAL; + break; + case thread_priority_e::high: + win32_priority = THREAD_PRIORITY_ABOVE_NORMAL; + break; + case thread_priority_e::critical: + win32_priority = THREAD_PRIORITY_HIGHEST; + break; + default: + BOOST_LOG(error) << "Unknown thread priority: "sv << (int) priority; + return; + } + + if (!SetThreadPriority(GetCurrentThread(), win32_priority)) { auto winerr = GetLastError(); - BOOST_LOG(warning) << "Unable to get current state of Mouse Keys: "sv << winerr; + BOOST_LOG(warning) << "Unable to set thread priority to "sv << win32_priority << ": "sv << winerr; } } -} -void streaming_will_stop() { - // Demote ourselves back to normal priority class - SetPriorityClass(GetCurrentProcess(), NORMAL_PRIORITY_CLASS); + void + streaming_will_start() { + static std::once_flag load_wlanapi_once_flag; + std::call_once(load_wlanapi_once_flag, []() { + // wlanapi.dll is not installed by default on Windows Server, so we load it dynamically + HMODULE wlanapi = LoadLibraryExA("wlanapi.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (!wlanapi) { + BOOST_LOG(debug) << "wlanapi.dll is not available on this OS"sv; + return; + } - // End our 1ms timer request - timeEndPeriod(1); + fn_WlanOpenHandle = (decltype(fn_WlanOpenHandle)) GetProcAddress(wlanapi, "WlanOpenHandle"); + fn_WlanCloseHandle = (decltype(fn_WlanCloseHandle)) GetProcAddress(wlanapi, "WlanCloseHandle"); + fn_WlanFreeMemory = (decltype(fn_WlanFreeMemory)) GetProcAddress(wlanapi, "WlanFreeMemory"); + fn_WlanEnumInterfaces = (decltype(fn_WlanEnumInterfaces)) GetProcAddress(wlanapi, "WlanEnumInterfaces"); + fn_WlanSetInterface = (decltype(fn_WlanSetInterface)) GetProcAddress(wlanapi, "WlanSetInterface"); - // Disable MMCSS scheduling for DWM - DwmEnableMMCSS(false); + if (!fn_WlanOpenHandle || !fn_WlanCloseHandle || !fn_WlanFreeMemory || !fn_WlanEnumInterfaces || !fn_WlanSetInterface) { + BOOST_LOG(error) << "wlanapi.dll is missing exports?"sv; - // Closing our WLAN client handle will undo our optimizations - if(wlan_handle != nullptr) { - fn_WlanCloseHandle(wlan_handle, nullptr); - wlan_handle = nullptr; + fn_WlanOpenHandle = nullptr; + fn_WlanCloseHandle = nullptr; + fn_WlanFreeMemory = nullptr; + fn_WlanEnumInterfaces = nullptr; + fn_WlanSetInterface = nullptr; + + FreeLibrary(wlanapi); + return; + } + }); + + // Enable MMCSS scheduling for DWM + DwmEnableMMCSS(true); + + // Reduce timer period to 1ms + timeBeginPeriod(1); + + // Promote ourselves to high priority class + SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); + + // Enable low latency mode on all connected WLAN NICs if wlanapi.dll is available + if (fn_WlanOpenHandle) { + DWORD negotiated_version; + + if (fn_WlanOpenHandle(WLAN_API_MAKE_VERSION(2, 0), nullptr, &negotiated_version, &wlan_handle) == ERROR_SUCCESS) { + PWLAN_INTERFACE_INFO_LIST wlan_interface_list; + + if (fn_WlanEnumInterfaces(wlan_handle, nullptr, &wlan_interface_list) == ERROR_SUCCESS) { + for (DWORD i = 0; i < wlan_interface_list->dwNumberOfItems; i++) { + if (wlan_interface_list->InterfaceInfo[i].isState == wlan_interface_state_connected) { + // Enable media streaming mode for 802.11 wireless interfaces to reduce latency and + // unneccessary background scanning operations that cause packet loss and jitter. + // + // https://docs.microsoft.com/en-us/windows-hardware/drivers/network/oid-wdi-set-connection-quality + // https://docs.microsoft.com/en-us/previous-versions/windows/hardware/wireless/native-802-11-media-streaming + BOOL value = TRUE; + auto error = fn_WlanSetInterface(wlan_handle, &wlan_interface_list->InterfaceInfo[i].InterfaceGuid, + wlan_intf_opcode_media_streaming_mode, sizeof(value), &value, nullptr); + if (error == ERROR_SUCCESS) { + BOOST_LOG(info) << "WLAN interface "sv << i << " is now in low latency mode"sv; + } + } + } + + fn_WlanFreeMemory(wlan_interface_list); + } + else { + fn_WlanCloseHandle(wlan_handle, nullptr); + wlan_handle = NULL; + } + } + } + + // If there is no mouse connected, enable Mouse Keys to force the cursor to appear + if (!GetSystemMetrics(SM_MOUSEPRESENT)) { + BOOST_LOG(info) << "A mouse was not detected. Sunshine will enable Mouse Keys while streaming to force the mouse cursor to appear."; + + // Get the current state of Mouse Keys so we can restore it when streaming is over + previous_mouse_keys_state.cbSize = sizeof(previous_mouse_keys_state); + if (SystemParametersInfoW(SPI_GETMOUSEKEYS, 0, &previous_mouse_keys_state, 0)) { + MOUSEKEYS new_mouse_keys_state = {}; + + // Enable Mouse Keys + new_mouse_keys_state.cbSize = sizeof(new_mouse_keys_state); + new_mouse_keys_state.dwFlags = MKF_MOUSEKEYSON | MKF_AVAILABLE; + new_mouse_keys_state.iMaxSpeed = 10; + new_mouse_keys_state.iTimeToMaxSpeed = 1000; + if (SystemParametersInfoW(SPI_SETMOUSEKEYS, 0, &new_mouse_keys_state, 0)) { + // Remember to restore the previous settings when we stop streaming + enabled_mouse_keys = true; + } + else { + auto winerr = GetLastError(); + BOOST_LOG(warning) << "Unable to enable Mouse Keys: "sv << winerr; + } + } + else { + auto winerr = GetLastError(); + BOOST_LOG(warning) << "Unable to get current state of Mouse Keys: "sv << winerr; + } + } } - // Restore Mouse Keys back to the previous settings if we turned it on - if(enabled_mouse_keys) { - enabled_mouse_keys = false; - if(!SystemParametersInfoW(SPI_SETMOUSEKEYS, 0, &previous_mouse_keys_state, 0)) { + void + streaming_will_stop() { + // Demote ourselves back to normal priority class + SetPriorityClass(GetCurrentProcess(), NORMAL_PRIORITY_CLASS); + + // End our 1ms timer request + timeEndPeriod(1); + + // Disable MMCSS scheduling for DWM + DwmEnableMMCSS(false); + + // Closing our WLAN client handle will undo our optimizations + if (wlan_handle != nullptr) { + fn_WlanCloseHandle(wlan_handle, nullptr); + wlan_handle = nullptr; + } + + // Restore Mouse Keys back to the previous settings if we turned it on + if (enabled_mouse_keys) { + enabled_mouse_keys = false; + if (!SystemParametersInfoW(SPI_SETMOUSEKEYS, 0, &previous_mouse_keys_state, 0)) { + auto winerr = GetLastError(); + BOOST_LOG(warning) << "Unable to restore original state of Mouse Keys: "sv << winerr; + } + } + } + + bool + restart_supported() { + // Restart is supported if we're running from the service + return (GetConsoleWindow() == NULL); + } + + bool + restart() { + // Raise SIGINT to trigger the graceful exit logic. The service will + // restart us in a few seconds. + std::raise(SIGINT); + return true; + } + + SOCKADDR_IN + to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { + SOCKADDR_IN saddr_v4 = {}; + + saddr_v4.sin_family = AF_INET; + saddr_v4.sin_port = htons(port); + + auto addr_bytes = address.to_bytes(); + memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr)); + + return saddr_v4; + } + + SOCKADDR_IN6 + to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { + SOCKADDR_IN6 saddr_v6 = {}; + + saddr_v6.sin6_family = AF_INET6; + saddr_v6.sin6_port = htons(port); + saddr_v6.sin6_scope_id = address.scope_id(); + + auto addr_bytes = address.to_bytes(); + memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr)); + + return saddr_v6; + } + + // Use UDP segmentation offload if it is supported by the OS. If the NIC is capable, this will use + // hardware acceleration to reduce CPU usage. Support for USO was introduced in Windows 10 20H1. + bool + send_batch(batched_send_info_t &send_info) { + WSAMSG msg; + + // Convert the target address into a SOCKADDR + SOCKADDR_IN saddr_v4; + SOCKADDR_IN6 saddr_v6; + if (send_info.target_address.is_v6()) { + saddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); + + msg.name = (PSOCKADDR) &saddr_v6; + msg.namelen = sizeof(saddr_v6); + } + else { + saddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); + + msg.name = (PSOCKADDR) &saddr_v4; + msg.namelen = sizeof(saddr_v4); + } + + WSABUF buf; + buf.buf = (char *) send_info.buffer; + buf.len = send_info.block_size * send_info.block_count; + + msg.lpBuffers = &buf; + msg.dwBufferCount = 1; + msg.dwFlags = 0; + + char cmbuf[WSA_CMSG_SPACE(sizeof(DWORD))]; + msg.Control.buf = cmbuf; + msg.Control.len = 0; + + if (send_info.block_count > 1) { + msg.Control.len += WSA_CMSG_SPACE(sizeof(DWORD)); + + auto cm = WSA_CMSG_FIRSTHDR(&msg); + cm->cmsg_level = IPPROTO_UDP; + cm->cmsg_type = UDP_SEND_MSG_SIZE; + cm->cmsg_len = WSA_CMSG_LEN(sizeof(DWORD)); + *((DWORD *) WSA_CMSG_DATA(cm)) = send_info.block_size; + } + + // If USO is not supported, this will fail and the caller will fall back to unbatched sends. + DWORD bytes_sent; + return WSASendMsg((SOCKET) send_info.native_socket, &msg, 1, &bytes_sent, nullptr, nullptr) != SOCKET_ERROR; + } + + class qos_t: public deinit_t { + public: + qos_t(QOS_FLOWID flow_id): + flow_id(flow_id) {} + + virtual ~qos_t() { + if (!fn_QOSRemoveSocketFromFlow(qos_handle, (SOCKET) NULL, flow_id, 0)) { + auto winerr = GetLastError(); + BOOST_LOG(warning) << "QOSRemoveSocketFromFlow() failed: "sv << winerr; + } + } + + private: + QOS_FLOWID flow_id; + }; + + std::unique_ptr + enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { + SOCKADDR_IN saddr_v4; + SOCKADDR_IN6 saddr_v6; + PSOCKADDR dest_addr; + + static std::once_flag load_qwave_once_flag; + std::call_once(load_qwave_once_flag, []() { + // qWAVE is not installed by default on Windows Server, so we load it dynamically + HMODULE qwave = LoadLibraryExA("qwave.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (!qwave) { + BOOST_LOG(debug) << "qwave.dll is not available on this OS"sv; + return; + } + + fn_QOSCreateHandle = (decltype(fn_QOSCreateHandle)) GetProcAddress(qwave, "QOSCreateHandle"); + fn_QOSAddSocketToFlow = (decltype(fn_QOSAddSocketToFlow)) GetProcAddress(qwave, "QOSAddSocketToFlow"); + fn_QOSRemoveSocketFromFlow = (decltype(fn_QOSRemoveSocketFromFlow)) GetProcAddress(qwave, "QOSRemoveSocketFromFlow"); + + if (!fn_QOSCreateHandle || !fn_QOSAddSocketToFlow || !fn_QOSRemoveSocketFromFlow) { + BOOST_LOG(error) << "qwave.dll is missing exports?"sv; + + fn_QOSCreateHandle = nullptr; + fn_QOSAddSocketToFlow = nullptr; + fn_QOSRemoveSocketFromFlow = nullptr; + + FreeLibrary(qwave); + return; + } + + QOS_VERSION qos_version { 1, 0 }; + if (!fn_QOSCreateHandle(&qos_version, &qos_handle)) { + auto winerr = GetLastError(); + BOOST_LOG(warning) << "QOSCreateHandle() failed: "sv << winerr; + return; + } + }); + + // If qWAVE is unavailable, just return + if (!fn_QOSAddSocketToFlow || !qos_handle) { + return nullptr; + } + + if (address.is_v6()) { + saddr_v6 = to_sockaddr(address.to_v6(), port); + dest_addr = (PSOCKADDR) &saddr_v6; + } + else { + saddr_v4 = to_sockaddr(address.to_v4(), port); + dest_addr = (PSOCKADDR) &saddr_v4; + } + + QOS_TRAFFIC_TYPE traffic_type; + switch (data_type) { + case qos_data_type_e::audio: + traffic_type = QOSTrafficTypeVoice; + break; + case qos_data_type_e::video: + traffic_type = QOSTrafficTypeAudioVideo; + break; + default: + BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; + return nullptr; + } + + QOS_FLOWID flow_id = 0; + if (!fn_QOSAddSocketToFlow(qos_handle, (SOCKET) native_socket, dest_addr, traffic_type, QOS_NON_ADAPTIVE_FLOW, &flow_id)) { auto winerr = GetLastError(); - BOOST_LOG(warning) << "Unable to restore original state of Mouse Keys: "sv << winerr; - } - } -} - -bool restart_supported() { - // Restart is supported if we're running from the service - return (GetConsoleWindow() == NULL); -} - -bool restart() { - // Raise SIGINT to trigger the graceful exit logic. The service will - // restart us in a few seconds. - std::raise(SIGINT); - return true; -} - -SOCKADDR_IN to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { - SOCKADDR_IN saddr_v4 = {}; - - saddr_v4.sin_family = AF_INET; - saddr_v4.sin_port = htons(port); - - auto addr_bytes = address.to_bytes(); - memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr)); - - return saddr_v4; -} - -SOCKADDR_IN6 to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { - SOCKADDR_IN6 saddr_v6 = {}; - - saddr_v6.sin6_family = AF_INET6; - saddr_v6.sin6_port = htons(port); - saddr_v6.sin6_scope_id = address.scope_id(); - - auto addr_bytes = address.to_bytes(); - memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr)); - - return saddr_v6; -} - -// Use UDP segmentation offload if it is supported by the OS. If the NIC is capable, this will use -// hardware acceleration to reduce CPU usage. Support for USO was introduced in Windows 10 20H1. -bool send_batch(batched_send_info_t &send_info) { - WSAMSG msg; - - // Convert the target address into a SOCKADDR - SOCKADDR_IN saddr_v4; - SOCKADDR_IN6 saddr_v6; - if(send_info.target_address.is_v6()) { - saddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); - - msg.name = (PSOCKADDR)&saddr_v6; - msg.namelen = sizeof(saddr_v6); - } - else { - saddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); - - msg.name = (PSOCKADDR)&saddr_v4; - msg.namelen = sizeof(saddr_v4); - } - - WSABUF buf; - buf.buf = (char *)send_info.buffer; - buf.len = send_info.block_size * send_info.block_count; - - msg.lpBuffers = &buf; - msg.dwBufferCount = 1; - msg.dwFlags = 0; - - char cmbuf[WSA_CMSG_SPACE(sizeof(DWORD))]; - msg.Control.buf = cmbuf; - msg.Control.len = 0; - - if(send_info.block_count > 1) { - msg.Control.len += WSA_CMSG_SPACE(sizeof(DWORD)); - - auto cm = WSA_CMSG_FIRSTHDR(&msg); - cm->cmsg_level = IPPROTO_UDP; - cm->cmsg_type = UDP_SEND_MSG_SIZE; - cm->cmsg_len = WSA_CMSG_LEN(sizeof(DWORD)); - *((DWORD *)WSA_CMSG_DATA(cm)) = send_info.block_size; - } - - // If USO is not supported, this will fail and the caller will fall back to unbatched sends. - DWORD bytes_sent; - return WSASendMsg((SOCKET)send_info.native_socket, &msg, 1, &bytes_sent, nullptr, nullptr) != SOCKET_ERROR; -} - -class qos_t : public deinit_t { -public: - qos_t(QOS_FLOWID flow_id) : flow_id(flow_id) {} - - virtual ~qos_t() { - if(!fn_QOSRemoveSocketFromFlow(qos_handle, (SOCKET)NULL, flow_id, 0)) { - auto winerr = GetLastError(); - BOOST_LOG(warning) << "QOSRemoveSocketFromFlow() failed: "sv << winerr; - } - } - -private: - QOS_FLOWID flow_id; -}; - -std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type) { - SOCKADDR_IN saddr_v4; - SOCKADDR_IN6 saddr_v6; - PSOCKADDR dest_addr; - - static std::once_flag load_qwave_once_flag; - std::call_once(load_qwave_once_flag, []() { - // qWAVE is not installed by default on Windows Server, so we load it dynamically - HMODULE qwave = LoadLibraryExA("qwave.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); - if(!qwave) { - BOOST_LOG(debug) << "qwave.dll is not available on this OS"sv; - return; + BOOST_LOG(warning) << "QOSAddSocketToFlow() failed: "sv << winerr; + return nullptr; } - fn_QOSCreateHandle = (decltype(fn_QOSCreateHandle))GetProcAddress(qwave, "QOSCreateHandle"); - fn_QOSAddSocketToFlow = (decltype(fn_QOSAddSocketToFlow))GetProcAddress(qwave, "QOSAddSocketToFlow"); - fn_QOSRemoveSocketFromFlow = (decltype(fn_QOSRemoveSocketFromFlow))GetProcAddress(qwave, "QOSRemoveSocketFromFlow"); - - if(!fn_QOSCreateHandle || !fn_QOSAddSocketToFlow || !fn_QOSRemoveSocketFromFlow) { - BOOST_LOG(error) << "qwave.dll is missing exports?"sv; - - fn_QOSCreateHandle = nullptr; - fn_QOSAddSocketToFlow = nullptr; - fn_QOSRemoveSocketFromFlow = nullptr; - - FreeLibrary(qwave); - return; - } - - QOS_VERSION qos_version { 1, 0 }; - if(!fn_QOSCreateHandle(&qos_version, &qos_handle)) { - auto winerr = GetLastError(); - BOOST_LOG(warning) << "QOSCreateHandle() failed: "sv << winerr; - return; - } - }); - - // If qWAVE is unavailable, just return - if(!fn_QOSAddSocketToFlow || !qos_handle) { - return nullptr; + return std::make_unique(flow_id); } - if(address.is_v6()) { - saddr_v6 = to_sockaddr(address.to_v6(), port); - dest_addr = (PSOCKADDR)&saddr_v6; - } - else { - saddr_v4 = to_sockaddr(address.to_v4(), port); - dest_addr = (PSOCKADDR)&saddr_v4; - } - - QOS_TRAFFIC_TYPE traffic_type; - switch(data_type) { - case qos_data_type_e::audio: - traffic_type = QOSTrafficTypeVoice; - break; - case qos_data_type_e::video: - traffic_type = QOSTrafficTypeAudioVideo; - break; - default: - BOOST_LOG(error) << "Unknown traffic type: "sv << (int)data_type; - return nullptr; - } - - QOS_FLOWID flow_id = 0; - if(!fn_QOSAddSocketToFlow(qos_handle, (SOCKET)native_socket, dest_addr, traffic_type, QOS_NON_ADAPTIVE_FLOW, &flow_id)) { - auto winerr = GetLastError(); - BOOST_LOG(warning) << "QOSAddSocketToFlow() failed: "sv << winerr; - return nullptr; - } - - return std::make_unique(flow_id); -} - -} // namespace platf \ No newline at end of file +} // namespace platf \ No newline at end of file diff --git a/src/platform/windows/misc.h b/src/platform/windows/misc.h index a045d23b..4bcd31fd 100644 --- a/src/platform/windows/misc.h +++ b/src/platform/windows/misc.h @@ -6,8 +6,10 @@ #include namespace platf { -void print_status(const std::string_view &prefix, HRESULT status); -HDESK syncThreadDesktop(); -} // namespace platf + void + print_status(const std::string_view &prefix, HRESULT status); + HDESK + syncThreadDesktop(); +} // namespace platf #endif \ No newline at end of file diff --git a/src/platform/windows/publish.cpp b/src/platform/windows/publish.cpp index d59c55c5..56cfdfc6 100644 --- a/src/platform/windows/publish.cpp +++ b/src/platform/windows/publish.cpp @@ -35,7 +35,7 @@ constexpr auto DNS_QUERY_RESULTS_VERSION1 = 0x1; #define SERVICE_DOMAIN "local" constexpr auto SERVICE_INSTANCE_NAME = SV(SERVICE_NAME "." SERVICE_TYPE "." SERVICE_DOMAIN); -constexpr auto SERVICE_TYPE_DOMAIN = SV(SERVICE_TYPE "." SERVICE_DOMAIN); +constexpr auto SERVICE_TYPE_DOMAIN = SV(SERVICE_TYPE "." SERVICE_DOMAIN); #ifndef __MINGW32__ typedef struct _DNS_SERVICE_INSTANCE { @@ -59,7 +59,8 @@ typedef struct _DNS_SERVICE_INSTANCE { } DNS_SERVICE_INSTANCE, *PDNS_SERVICE_INSTANCE; #endif -typedef VOID WINAPI DNS_SERVICE_REGISTER_COMPLETE( +typedef VOID WINAPI +DNS_SERVICE_REGISTER_COMPLETE( _In_ DWORD Status, _In_ PVOID pQueryContext, _In_ PDNS_SERVICE_INSTANCE pInstance); @@ -88,122 +89,127 @@ _FN(_DnsServiceRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _I } /* extern "C" */ namespace platf::publish { -VOID WINAPI register_cb(DWORD status, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) { - auto alarm = (safe::alarm_t::element_type *)pQueryContext; + VOID WINAPI + register_cb(DWORD status, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) { + auto alarm = (safe::alarm_t::element_type *) pQueryContext; - if(status) { - print_status("register_cb()"sv, status); - } - - alarm->ring(pInstance); -} - -static int service(bool enable, PDNS_SERVICE_INSTANCE &existing_instance) { - auto alarm = safe::make_alarm(); - - std::wstring_convert, wchar_t> converter; - - std::wstring name { SERVICE_INSTANCE_NAME.data(), SERVICE_INSTANCE_NAME.size() }; - std::wstring domain { SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size() }; - - auto host = converter.from_bytes(boost::asio::ip::host_name() + ".local"); - - DNS_SERVICE_INSTANCE instance {}; - instance.pszInstanceName = name.data(); - instance.wPort = map_port(nvhttp::PORT_HTTP); - instance.pszHostName = host.data(); - - DNS_SERVICE_REGISTER_REQUEST req {}; - req.Version = DNS_QUERY_REQUEST_VERSION1; - req.pQueryContext = alarm.get(); - req.pServiceInstance = enable ? &instance : existing_instance; - req.pRegisterCompletionCallback = register_cb; - - DNS_STATUS status {}; - - if(enable) { - status = _DnsServiceRegister(&req, nullptr); - if(status != DNS_REQUEST_PENDING) { - print_status("DnsServiceRegister()"sv, status); - return -1; - } - } - else { - status = _DnsServiceDeRegister(&req, nullptr); - if(status != DNS_REQUEST_PENDING) { - print_status("DnsServiceDeRegister()"sv, status); - return -1; - } - } - - alarm->wait(); - - auto registered_instance = alarm->status(); - if(enable) { - // Store this instance for later deregistration - existing_instance = registered_instance; - } - else if(registered_instance) { - // Deregistration was successful - _DnsServiceFreeInstance(registered_instance); - existing_instance = nullptr; - } - - return registered_instance ? 0 : -1; -} - -class mdns_registration_t : public ::platf::deinit_t { -public: - mdns_registration_t() : existing_instance(nullptr) { - if(service(true, existing_instance)) { - BOOST_LOG(error) << "Unable to register Sunshine mDNS service"sv; - return; + if (status) { + print_status("register_cb()"sv, status); } - BOOST_LOG(info) << "Registered Sunshine mDNS service"sv; + alarm->ring(pInstance); } - ~mdns_registration_t() override { - if(existing_instance) { - if(service(false, existing_instance)) { - BOOST_LOG(error) << "Unable to unregister Sunshine mDNS service"sv; + static int + service(bool enable, PDNS_SERVICE_INSTANCE &existing_instance) { + auto alarm = safe::make_alarm(); + + std::wstring_convert, wchar_t> converter; + + std::wstring name { SERVICE_INSTANCE_NAME.data(), SERVICE_INSTANCE_NAME.size() }; + std::wstring domain { SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size() }; + + auto host = converter.from_bytes(boost::asio::ip::host_name() + ".local"); + + DNS_SERVICE_INSTANCE instance {}; + instance.pszInstanceName = name.data(); + instance.wPort = map_port(nvhttp::PORT_HTTP); + instance.pszHostName = host.data(); + + DNS_SERVICE_REGISTER_REQUEST req {}; + req.Version = DNS_QUERY_REQUEST_VERSION1; + req.pQueryContext = alarm.get(); + req.pServiceInstance = enable ? &instance : existing_instance; + req.pRegisterCompletionCallback = register_cb; + + DNS_STATUS status {}; + + if (enable) { + status = _DnsServiceRegister(&req, nullptr); + if (status != DNS_REQUEST_PENDING) { + print_status("DnsServiceRegister()"sv, status); + return -1; + } + } + else { + status = _DnsServiceDeRegister(&req, nullptr); + if (status != DNS_REQUEST_PENDING) { + print_status("DnsServiceDeRegister()"sv, status); + return -1; + } + } + + alarm->wait(); + + auto registered_instance = alarm->status(); + if (enable) { + // Store this instance for later deregistration + existing_instance = registered_instance; + } + else if (registered_instance) { + // Deregistration was successful + _DnsServiceFreeInstance(registered_instance); + existing_instance = nullptr; + } + + return registered_instance ? 0 : -1; + } + + class mdns_registration_t: public ::platf::deinit_t { + public: + mdns_registration_t(): + existing_instance(nullptr) { + if (service(true, existing_instance)) { + BOOST_LOG(error) << "Unable to register Sunshine mDNS service"sv; return; } - BOOST_LOG(info) << "Unregistered Sunshine mDNS service"sv; + BOOST_LOG(info) << "Registered Sunshine mDNS service"sv; } + + ~mdns_registration_t() override { + if (existing_instance) { + if (service(false, existing_instance)) { + BOOST_LOG(error) << "Unable to unregister Sunshine mDNS service"sv; + return; + } + + BOOST_LOG(info) << "Unregistered Sunshine mDNS service"sv; + } + } + + private: + PDNS_SERVICE_INSTANCE existing_instance; + }; + + int + load_funcs(HMODULE handle) { + auto fg = util::fail_guard([handle]() { + FreeLibrary(handle); + }); + + _DnsServiceFreeInstance = (_DnsServiceFreeInstance_fn) GetProcAddress(handle, "DnsServiceFreeInstance"); + _DnsServiceDeRegister = (_DnsServiceDeRegister_fn) GetProcAddress(handle, "DnsServiceDeRegister"); + _DnsServiceRegister = (_DnsServiceRegister_fn) GetProcAddress(handle, "DnsServiceRegister"); + + if (!(_DnsServiceFreeInstance && _DnsServiceDeRegister && _DnsServiceRegister)) { + BOOST_LOG(error) << "mDNS service not available in dnsapi.dll"sv; + return -1; + } + + fg.disable(); + return 0; } -private: - PDNS_SERVICE_INSTANCE existing_instance; -}; + std::unique_ptr<::platf::deinit_t> + start() { + HMODULE handle = LoadLibrary("dnsapi.dll"); -int load_funcs(HMODULE handle) { - auto fg = util::fail_guard([handle]() { - FreeLibrary(handle); - }); + if (!handle || load_funcs(handle)) { + BOOST_LOG(error) << "Couldn't load dnsapi.dll, You'll need to add PC manually from Moonlight"sv; + return nullptr; + } - _DnsServiceFreeInstance = (_DnsServiceFreeInstance_fn)GetProcAddress(handle, "DnsServiceFreeInstance"); - _DnsServiceDeRegister = (_DnsServiceDeRegister_fn)GetProcAddress(handle, "DnsServiceDeRegister"); - _DnsServiceRegister = (_DnsServiceRegister_fn)GetProcAddress(handle, "DnsServiceRegister"); - - if(!(_DnsServiceFreeInstance && _DnsServiceDeRegister && _DnsServiceRegister)) { - BOOST_LOG(error) << "mDNS service not available in dnsapi.dll"sv; - return -1; + return std::make_unique(); } - - fg.disable(); - return 0; -} - -std::unique_ptr<::platf::deinit_t> start() { - HMODULE handle = LoadLibrary("dnsapi.dll"); - - if(!handle || load_funcs(handle)) { - BOOST_LOG(error) << "Couldn't load dnsapi.dll, You'll need to add PC manually from Moonlight"sv; - return nullptr; - } - - return std::make_unique(); -} -} // namespace platf::publish +} // namespace platf::publish diff --git a/src/process.cpp b/src/process.cpp index 32ab32ff..6053e73b 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -25,536 +25,552 @@ #include "utility.h" #ifdef _WIN32 -// _SH constants for _wfsopen() -#include + // _SH constants for _wfsopen() + #include #endif #define DEFAULT_APP_IMAGE_PATH SUNSHINE_ASSETS_DIR "/box.png" namespace proc { -using namespace std::literals; -namespace bp = boost::process; -namespace pt = boost::property_tree; + using namespace std::literals; + namespace bp = boost::process; + namespace pt = boost::property_tree; -proc_t proc; + proc_t proc; -void process_end(bp::child &proc, bp::group &proc_handle) { - if(!proc.running()) { - return; + void + process_end(bp::child &proc, bp::group &proc_handle) { + if (!proc.running()) { + return; + } + + BOOST_LOG(debug) << "Force termination Child-Process"sv; + proc_handle.terminate(); + + // avoid zombie process + proc.wait(); } - BOOST_LOG(debug) << "Force termination Child-Process"sv; - proc_handle.terminate(); - - // avoid zombie process - proc.wait(); -} - -boost::filesystem::path find_working_directory(const std::string &cmd, bp::environment &env) { - // Parse the raw command string into parts to get the actual command portion + boost::filesystem::path + find_working_directory(const std::string &cmd, bp::environment &env) { + // Parse the raw command string into parts to get the actual command portion #ifdef _WIN32 - auto parts = boost::program_options::split_winmain(cmd); + auto parts = boost::program_options::split_winmain(cmd); #else - auto parts = boost::program_options::split_unix(cmd); + auto parts = boost::program_options::split_unix(cmd); #endif - if(parts.empty()) { - BOOST_LOG(error) << "Unable to parse command: "sv << cmd; - return boost::filesystem::path(); - } - - BOOST_LOG(debug) << "Parsed executable ["sv << parts.at(0) << "] from command ["sv << cmd << ']'; - - // If the cmd path is not an absolute path, resolve it using our PATH variable - boost::filesystem::path cmd_path(parts.at(0)); - if(!cmd_path.is_absolute()) { - cmd_path = boost::process::search_path(parts.at(0)); - if(cmd_path.empty()) { - BOOST_LOG(error) << "Unable to find executable ["sv << parts.at(0) << "]. Is it in your PATH?"sv; + if (parts.empty()) { + BOOST_LOG(error) << "Unable to parse command: "sv << cmd; return boost::filesystem::path(); } + + BOOST_LOG(debug) << "Parsed executable ["sv << parts.at(0) << "] from command ["sv << cmd << ']'; + + // If the cmd path is not an absolute path, resolve it using our PATH variable + boost::filesystem::path cmd_path(parts.at(0)); + if (!cmd_path.is_absolute()) { + cmd_path = boost::process::search_path(parts.at(0)); + if (cmd_path.empty()) { + BOOST_LOG(error) << "Unable to find executable ["sv << parts.at(0) << "]. Is it in your PATH?"sv; + return boost::filesystem::path(); + } + } + + BOOST_LOG(debug) << "Resolved executable ["sv << parts.at(0) << "] to path ["sv << cmd_path << ']'; + + // Now that we have a complete path, we can just use parent_path() + return cmd_path.parent_path(); } - BOOST_LOG(debug) << "Resolved executable ["sv << parts.at(0) << "] to path ["sv << cmd_path << ']'; + int + proc_t::execute(int app_id) { + // Ensure starting from a clean slate + terminate(); - // Now that we have a complete path, we can just use parent_path() - return cmd_path.parent_path(); -} + auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) { + return app.id == std::to_string(app_id); + }); -int proc_t::execute(int app_id) { - // Ensure starting from a clean slate - terminate(); + if (iter == _apps.end()) { + BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']'; + return 404; + } - auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) { - return app.id == std::to_string(app_id); - }); + _app_id = app_id; + _app = *iter; - if(iter == _apps.end()) { - BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']'; - return 404; - } + _app_prep_begin = std::begin(_app.prep_cmds); + _app_prep_it = _app_prep_begin; - _app_id = app_id; - _app = *iter; - - _app_prep_begin = std::begin(_app.prep_cmds); - _app_prep_it = _app_prep_begin; - - if(!_app.output.empty() && _app.output != "null"sv) { + if (!_app.output.empty() && _app.output != "null"sv) { #ifdef _WIN32 - // fopen() interprets the filename as an ANSI string on Windows, so we must convert it - // to UTF-16 and use the wchar_t variants for proper Unicode log file path support. - std::wstring_convert, wchar_t> converter; - auto woutput = converter.from_bytes(_app.output); + // fopen() interprets the filename as an ANSI string on Windows, so we must convert it + // to UTF-16 and use the wchar_t variants for proper Unicode log file path support. + std::wstring_convert, wchar_t> converter; + auto woutput = converter.from_bytes(_app.output); - // Use _SH_DENYNO to allow us to open this log file again for writing even if it is - // still open from a previous execution. This is required to handle the case of a - // detached process executing again while the previous process is still running. - _pipe.reset(_wfsopen(woutput.c_str(), L"a", _SH_DENYNO)); + // Use _SH_DENYNO to allow us to open this log file again for writing even if it is + // still open from a previous execution. This is required to handle the case of a + // detached process executing again while the previous process is still running. + _pipe.reset(_wfsopen(woutput.c_str(), L"a", _SH_DENYNO)); #else - _pipe.reset(fopen(_app.output.c_str(), "a")); + _pipe.reset(fopen(_app.output.c_str(), "a")); #endif - } - - std::error_code ec; - // Executed when returning from function - auto fg = util::fail_guard([&]() { - terminate(); - }); - - for(; _app_prep_it != std::end(_app.prep_cmds); ++_app_prep_it) { - auto &cmd = _app_prep_it->do_cmd; - - boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(cmd, _env) : - boost::filesystem::path(_app.working_dir); - BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd << ']'; - auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr); - - if(ec) { - BOOST_LOG(error) << "Couldn't run ["sv << cmd << "]: System: "sv << ec.message(); - return -1; } - child.wait(); - auto ret = child.exit_code(); + std::error_code ec; + // Executed when returning from function + auto fg = util::fail_guard([&]() { + terminate(); + }); - if(ret != 0) { - BOOST_LOG(error) << '[' << cmd << "] failed with code ["sv << ret << ']'; - return -1; - } - } + for (; _app_prep_it != std::end(_app.prep_cmds); ++_app_prep_it) { + auto &cmd = _app_prep_it->do_cmd; - for(auto &cmd : _app.detached) { - boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(cmd, _env) : - boost::filesystem::path(_app.working_dir); - BOOST_LOG(info) << "Spawning ["sv << cmd << "] in ["sv << working_dir << ']'; - auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr); - if(ec) { - BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message(); - } - else { - child.detach(); - } - } + boost::filesystem::path working_dir = _app.working_dir.empty() ? + find_working_directory(cmd, _env) : + boost::filesystem::path(_app.working_dir); + BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd << ']'; + auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr); - if(_app.cmd.empty()) { - BOOST_LOG(info) << "Executing [Desktop]"sv; - placebo = true; - } - else { - boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(_app.cmd, _env) : - boost::filesystem::path(_app.working_dir); - BOOST_LOG(info) << "Executing: ["sv << _app.cmd << "] in ["sv << working_dir << ']'; - _process = platf::run_unprivileged(_app.cmd, working_dir, _env, _pipe.get(), ec, &_process_handle); - if(ec) { - BOOST_LOG(warning) << "Couldn't run ["sv << _app.cmd << "]: System: "sv << ec.message(); - return -1; - } - } - - fg.disable(); - - return 0; -} - -int proc_t::running() { - if(placebo || _process.running()) { - return _app_id; - } - - // Perform cleanup actions now if needed - if(_process) { - terminate(); - } - - return 0; -} - -void proc_t::terminate() { - std::error_code ec; - - // Ensure child process is terminated - placebo = false; - process_end(_process, _process_handle); - _process = bp::child(); - _process_handle = bp::group(); - _app_id = -1; - - for(; _app_prep_it != _app_prep_begin; --_app_prep_it) { - auto &cmd = (_app_prep_it - 1)->undo_cmd; - - if(cmd.empty()) { - continue; - } - - boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(cmd, _env) : - boost::filesystem::path(_app.working_dir); - BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd << ']'; - auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr); - - if(ec) { - BOOST_LOG(warning) << "System: "sv << ec.message(); - } - - child.wait(); - auto ret = child.exit_code(); - - if(ret != 0) { - BOOST_LOG(warning) << "Return code ["sv << ret << ']'; - } - } - - _pipe.reset(); -} - -const std::vector &proc_t::get_apps() const { - return _apps; -} -std::vector &proc_t::get_apps() { - return _apps; -} - -// Gets application image from application list. -// Returns image from assets directory if found there. -// Returns default image if image configuration is not set. -// Returns http content-type header compatible image type. -std::string proc_t::get_app_image(int app_id) { - auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) { - return app.id == std::to_string(app_id); - }); - auto app_image_path = iter == _apps.end() ? std::string() : iter->image_path; - - return validate_app_image_path(app_image_path); -} - -proc_t::~proc_t() { - terminate(); -} - -std::string_view::iterator find_match(std::string_view::iterator begin, std::string_view::iterator end) { - int stack = 0; - - --begin; - do { - ++begin; - switch(*begin) { - case '(': - ++stack; - break; - case ')': - --stack; - } - } while(begin != end && stack != 0); - - if(begin == end) { - throw std::out_of_range("Missing closing bracket \')\'"); - } - return begin; -} - -std::string parse_env_val(bp::native_environment &env, const std::string_view &val_raw) { - auto pos = std::begin(val_raw); - auto dollar = std::find(pos, std::end(val_raw), '$'); - - std::stringstream ss; - - while(dollar != std::end(val_raw)) { - auto next = dollar + 1; - if(next != std::end(val_raw)) { - switch(*next) { - case '(': { - ss.write(pos, (dollar - pos)); - auto var_begin = next + 1; - auto var_end = find_match(next, std::end(val_raw)); - auto var_name = std::string { var_begin, var_end }; - -#ifdef _WIN32 - // Windows treats environment variable names in a case-insensitive manner, - // so we look for a case-insensitive match here. This is critical for - // correctly appending to PATH on Windows. - auto itr = std::find_if(env.cbegin(), env.cend(), - [&](const auto &e) { return boost::iequals(e.get_name(), var_name); }); - if(itr != env.cend()) { - // Use an existing case-insensitive match - var_name = itr->get_name(); - } -#endif - - ss << env[var_name].to_string(); - - pos = var_end + 1; - next = var_end; - - break; - } - case '$': - ss.write(pos, (next - pos)); - pos = next + 1; - ++next; - break; + if (ec) { + BOOST_LOG(error) << "Couldn't run ["sv << cmd << "]: System: "sv << ec.message(); + return -1; } - dollar = std::find(next, std::end(val_raw), '$'); - } - else { - dollar = next; - } - } + child.wait(); + auto ret = child.exit_code(); - ss.write(pos, (dollar - pos)); - - return ss.str(); -} - -std::string validate_app_image_path(std::string app_image_path) { - if(app_image_path.empty()) { - return DEFAULT_APP_IMAGE_PATH; - } - - // get the image extension and convert it to lowercase - auto image_extension = std::filesystem::path(app_image_path).extension().string(); - boost::to_lower(image_extension); - - // return the default box image if extension is not "png" - if(image_extension != ".png") { - return DEFAULT_APP_IMAGE_PATH; - } - - // check if image is in assets directory - auto full_image_path = std::filesystem::path(SUNSHINE_ASSETS_DIR) / app_image_path; - if(std::filesystem::exists(full_image_path)) { - return full_image_path.string(); - } - else if(app_image_path == "./assets/steam.png") { - // handle old default steam image definition - return SUNSHINE_ASSETS_DIR "/steam.png"; - } - - // check if specified image exists - std::error_code code; - if(!std::filesystem::exists(app_image_path, code)) { - // return default box image if image does not exist - BOOST_LOG(warning) << "Couldn't find app image at path ["sv << app_image_path << ']'; - return DEFAULT_APP_IMAGE_PATH; - } - - // image is a png, and not in assets directory - // return only "content-type" http header compatible image type - return app_image_path; -} - -std::optional calculate_sha256(const std::string &filename) { - crypto::md_ctx_t ctx { EVP_MD_CTX_create() }; - if(!ctx) { - return std::nullopt; - } - - if(!EVP_DigestInit_ex(ctx.get(), EVP_sha256(), nullptr)) { - return std::nullopt; - } - - // Read file and update calculated SHA - char buf[1024 * 16]; - std::ifstream file(filename, std::ifstream::binary); - while(file.good()) { - file.read(buf, sizeof(buf)); - if(!EVP_DigestUpdate(ctx.get(), buf, file.gcount())) { - return std::nullopt; - } - } - file.close(); - - unsigned char result[SHA256_DIGEST_LENGTH]; - if(!EVP_DigestFinal_ex(ctx.get(), result, nullptr)) { - return std::nullopt; - } - - // Transform byte-array to string - std::stringstream ss; - ss << std::hex << std::setfill('0'); - for(const auto &byte : result) { - ss << std::setw(2) << (int)byte; - } - return ss.str(); -} - -uint32_t calculate_crc32(const std::string &input) { - boost::crc_32_type result; - result.process_bytes(input.data(), input.length()); - return result.checksum(); -} - -std::tuple calculate_app_id(const std::string &app_name, std::string app_image_path, int index) { - // Generate id by hashing name with image data if present - std::vector to_hash; - to_hash.push_back(app_name); - auto file_path = validate_app_image_path(app_image_path); - if(file_path != DEFAULT_APP_IMAGE_PATH) { - auto file_hash = calculate_sha256(file_path); - if(file_hash) { - to_hash.push_back(file_hash.value()); - } - else { - // Fallback to just hashing image path - to_hash.push_back(file_path); - } - } - - // Create combined strings for hash - std::stringstream ss; - for_each(to_hash.begin(), to_hash.end(), [&ss](const std::string &s) { ss << s; }); - auto input_no_index = ss.str(); - ss << index; - auto input_with_index = ss.str(); - - // CRC32 then truncate to signed 32-bit range due to client limitations - auto id_no_index = std::to_string(abs((int32_t)calculate_crc32(input_no_index))); - auto id_with_index = std::to_string(abs((int32_t)calculate_crc32(input_with_index))); - - return std::make_tuple(id_no_index, id_with_index); -} - -std::optional parse(const std::string &file_name) { - pt::ptree tree; - - try { - pt::read_json(file_name, tree); - - auto &apps_node = tree.get_child("apps"s); - auto &env_vars = tree.get_child("env"s); - - auto this_env = boost::this_process::environment(); - - for(auto &[name, val] : env_vars) { - this_env[name] = parse_env_val(this_env, val.get_value()); + if (ret != 0) { + BOOST_LOG(error) << '[' << cmd << "] failed with code ["sv << ret << ']'; + return -1; + } } - std::set ids; - std::vector apps; - int i = 0; - for(auto &[_, app_node] : apps_node) { - proc::ctx_t ctx; - - auto prep_nodes_opt = app_node.get_child_optional("prep-cmd"s); - auto detached_nodes_opt = app_node.get_child_optional("detached"s); - auto exclude_global_prep = app_node.get_optional("exclude-global-prep-cmd"s); - auto output = app_node.get_optional("output"s); - auto name = parse_env_val(this_env, app_node.get("name"s)); - auto cmd = app_node.get_optional("cmd"s); - auto image_path = app_node.get_optional("image-path"s); - auto working_dir = app_node.get_optional("working-dir"s); - - std::vector prep_cmds; - if(!exclude_global_prep.value_or(false)) { - prep_cmds.reserve(config::sunshine.prep_cmds.size()); - for(auto &prep_cmd : config::sunshine.prep_cmds) { - auto do_cmd = parse_env_val(this_env, prep_cmd.do_cmd); - auto undo_cmd = parse_env_val(this_env, prep_cmd.undo_cmd); - - prep_cmds.emplace_back(std::move(do_cmd), std::move(undo_cmd)); - } - } - - if(prep_nodes_opt) { - auto &prep_nodes = *prep_nodes_opt; - - prep_cmds.reserve(prep_cmds.size() + prep_nodes.size()); - for(auto &[_, prep_node] : prep_nodes) { - auto do_cmd = parse_env_val(this_env, prep_node.get("do"s)); - auto undo_cmd = prep_node.get_optional("undo"s); - - if(undo_cmd) { - prep_cmds.emplace_back(std::move(do_cmd), parse_env_val(this_env, *undo_cmd)); - } - else { - prep_cmds.emplace_back(std::move(do_cmd)); - } - } - } - - std::vector detached; - if(detached_nodes_opt) { - auto &detached_nodes = *detached_nodes_opt; - - detached.reserve(detached_nodes.size()); - for(auto &[_, detached_val] : detached_nodes) { - detached.emplace_back(parse_env_val(this_env, detached_val.get_value())); - } - } - - if(output) { - ctx.output = parse_env_val(this_env, *output); - } - - if(cmd) { - ctx.cmd = parse_env_val(this_env, *cmd); - } - - if(working_dir) { - ctx.working_dir = parse_env_val(this_env, *working_dir); - } - - if(image_path) { - ctx.image_path = parse_env_val(this_env, *image_path); - } - - auto possible_ids = calculate_app_id(name, ctx.image_path, i++); - if(ids.count(std::get<0>(possible_ids)) == 0) { - // Avoid using index to generate id if possible - ctx.id = std::get<0>(possible_ids); + for (auto &cmd : _app.detached) { + boost::filesystem::path working_dir = _app.working_dir.empty() ? + find_working_directory(cmd, _env) : + boost::filesystem::path(_app.working_dir); + BOOST_LOG(info) << "Spawning ["sv << cmd << "] in ["sv << working_dir << ']'; + auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr); + if (ec) { + BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message(); } else { - // Fallback to include index on collision - ctx.id = std::get<1>(possible_ids); + child.detach(); } - ids.insert(ctx.id); - - ctx.name = std::move(name); - ctx.prep_cmds = std::move(prep_cmds); - ctx.detached = std::move(detached); - - apps.emplace_back(std::move(ctx)); } - return proc::proc_t { - std::move(this_env), std::move(apps) - }; - } - catch(std::exception &e) { - BOOST_LOG(error) << e.what(); + if (_app.cmd.empty()) { + BOOST_LOG(info) << "Executing [Desktop]"sv; + placebo = true; + } + else { + boost::filesystem::path working_dir = _app.working_dir.empty() ? + find_working_directory(_app.cmd, _env) : + boost::filesystem::path(_app.working_dir); + BOOST_LOG(info) << "Executing: ["sv << _app.cmd << "] in ["sv << working_dir << ']'; + _process = platf::run_unprivileged(_app.cmd, working_dir, _env, _pipe.get(), ec, &_process_handle); + if (ec) { + BOOST_LOG(warning) << "Couldn't run ["sv << _app.cmd << "]: System: "sv << ec.message(); + return -1; + } + } + + fg.disable(); + + return 0; } - return std::nullopt; -} + int + proc_t::running() { + if (placebo || _process.running()) { + return _app_id; + } -void refresh(const std::string &file_name) { - auto proc_opt = proc::parse(file_name); + // Perform cleanup actions now if needed + if (_process) { + terminate(); + } - if(proc_opt) { - proc = std::move(*proc_opt); + return 0; } -} -} // namespace proc + + void + proc_t::terminate() { + std::error_code ec; + + // Ensure child process is terminated + placebo = false; + process_end(_process, _process_handle); + _process = bp::child(); + _process_handle = bp::group(); + _app_id = -1; + + for (; _app_prep_it != _app_prep_begin; --_app_prep_it) { + auto &cmd = (_app_prep_it - 1)->undo_cmd; + + if (cmd.empty()) { + continue; + } + + boost::filesystem::path working_dir = _app.working_dir.empty() ? + find_working_directory(cmd, _env) : + boost::filesystem::path(_app.working_dir); + BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd << ']'; + auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr); + + if (ec) { + BOOST_LOG(warning) << "System: "sv << ec.message(); + } + + child.wait(); + auto ret = child.exit_code(); + + if (ret != 0) { + BOOST_LOG(warning) << "Return code ["sv << ret << ']'; + } + } + + _pipe.reset(); + } + + const std::vector & + proc_t::get_apps() const { + return _apps; + } + std::vector & + proc_t::get_apps() { + return _apps; + } + + // Gets application image from application list. + // Returns image from assets directory if found there. + // Returns default image if image configuration is not set. + // Returns http content-type header compatible image type. + std::string + proc_t::get_app_image(int app_id) { + auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) { + return app.id == std::to_string(app_id); + }); + auto app_image_path = iter == _apps.end() ? std::string() : iter->image_path; + + return validate_app_image_path(app_image_path); + } + + proc_t::~proc_t() { + terminate(); + } + + std::string_view::iterator + find_match(std::string_view::iterator begin, std::string_view::iterator end) { + int stack = 0; + + --begin; + do { + ++begin; + switch (*begin) { + case '(': + ++stack; + break; + case ')': + --stack; + } + } while (begin != end && stack != 0); + + if (begin == end) { + throw std::out_of_range("Missing closing bracket \')\'"); + } + return begin; + } + + std::string + parse_env_val(bp::native_environment &env, const std::string_view &val_raw) { + auto pos = std::begin(val_raw); + auto dollar = std::find(pos, std::end(val_raw), '$'); + + std::stringstream ss; + + while (dollar != std::end(val_raw)) { + auto next = dollar + 1; + if (next != std::end(val_raw)) { + switch (*next) { + case '(': { + ss.write(pos, (dollar - pos)); + auto var_begin = next + 1; + auto var_end = find_match(next, std::end(val_raw)); + auto var_name = std::string { var_begin, var_end }; + +#ifdef _WIN32 + // Windows treats environment variable names in a case-insensitive manner, + // so we look for a case-insensitive match here. This is critical for + // correctly appending to PATH on Windows. + auto itr = std::find_if(env.cbegin(), env.cend(), + [&](const auto &e) { return boost::iequals(e.get_name(), var_name); }); + if (itr != env.cend()) { + // Use an existing case-insensitive match + var_name = itr->get_name(); + } +#endif + + ss << env[var_name].to_string(); + + pos = var_end + 1; + next = var_end; + + break; + } + case '$': + ss.write(pos, (next - pos)); + pos = next + 1; + ++next; + break; + } + + dollar = std::find(next, std::end(val_raw), '$'); + } + else { + dollar = next; + } + } + + ss.write(pos, (dollar - pos)); + + return ss.str(); + } + + std::string + validate_app_image_path(std::string app_image_path) { + if (app_image_path.empty()) { + return DEFAULT_APP_IMAGE_PATH; + } + + // get the image extension and convert it to lowercase + auto image_extension = std::filesystem::path(app_image_path).extension().string(); + boost::to_lower(image_extension); + + // return the default box image if extension is not "png" + if (image_extension != ".png") { + return DEFAULT_APP_IMAGE_PATH; + } + + // check if image is in assets directory + auto full_image_path = std::filesystem::path(SUNSHINE_ASSETS_DIR) / app_image_path; + if (std::filesystem::exists(full_image_path)) { + return full_image_path.string(); + } + else if (app_image_path == "./assets/steam.png") { + // handle old default steam image definition + return SUNSHINE_ASSETS_DIR "/steam.png"; + } + + // check if specified image exists + std::error_code code; + if (!std::filesystem::exists(app_image_path, code)) { + // return default box image if image does not exist + BOOST_LOG(warning) << "Couldn't find app image at path ["sv << app_image_path << ']'; + return DEFAULT_APP_IMAGE_PATH; + } + + // image is a png, and not in assets directory + // return only "content-type" http header compatible image type + return app_image_path; + } + + std::optional + calculate_sha256(const std::string &filename) { + crypto::md_ctx_t ctx { EVP_MD_CTX_create() }; + if (!ctx) { + return std::nullopt; + } + + if (!EVP_DigestInit_ex(ctx.get(), EVP_sha256(), nullptr)) { + return std::nullopt; + } + + // Read file and update calculated SHA + char buf[1024 * 16]; + std::ifstream file(filename, std::ifstream::binary); + while (file.good()) { + file.read(buf, sizeof(buf)); + if (!EVP_DigestUpdate(ctx.get(), buf, file.gcount())) { + return std::nullopt; + } + } + file.close(); + + unsigned char result[SHA256_DIGEST_LENGTH]; + if (!EVP_DigestFinal_ex(ctx.get(), result, nullptr)) { + return std::nullopt; + } + + // Transform byte-array to string + std::stringstream ss; + ss << std::hex << std::setfill('0'); + for (const auto &byte : result) { + ss << std::setw(2) << (int) byte; + } + return ss.str(); + } + + uint32_t + calculate_crc32(const std::string &input) { + boost::crc_32_type result; + result.process_bytes(input.data(), input.length()); + return result.checksum(); + } + + std::tuple + calculate_app_id(const std::string &app_name, std::string app_image_path, int index) { + // Generate id by hashing name with image data if present + std::vector to_hash; + to_hash.push_back(app_name); + auto file_path = validate_app_image_path(app_image_path); + if (file_path != DEFAULT_APP_IMAGE_PATH) { + auto file_hash = calculate_sha256(file_path); + if (file_hash) { + to_hash.push_back(file_hash.value()); + } + else { + // Fallback to just hashing image path + to_hash.push_back(file_path); + } + } + + // Create combined strings for hash + std::stringstream ss; + for_each(to_hash.begin(), to_hash.end(), [&ss](const std::string &s) { ss << s; }); + auto input_no_index = ss.str(); + ss << index; + auto input_with_index = ss.str(); + + // CRC32 then truncate to signed 32-bit range due to client limitations + auto id_no_index = std::to_string(abs((int32_t) calculate_crc32(input_no_index))); + auto id_with_index = std::to_string(abs((int32_t) calculate_crc32(input_with_index))); + + return std::make_tuple(id_no_index, id_with_index); + } + + std::optional + parse(const std::string &file_name) { + pt::ptree tree; + + try { + pt::read_json(file_name, tree); + + auto &apps_node = tree.get_child("apps"s); + auto &env_vars = tree.get_child("env"s); + + auto this_env = boost::this_process::environment(); + + for (auto &[name, val] : env_vars) { + this_env[name] = parse_env_val(this_env, val.get_value()); + } + + std::set ids; + std::vector apps; + int i = 0; + for (auto &[_, app_node] : apps_node) { + proc::ctx_t ctx; + + auto prep_nodes_opt = app_node.get_child_optional("prep-cmd"s); + auto detached_nodes_opt = app_node.get_child_optional("detached"s); + auto exclude_global_prep = app_node.get_optional("exclude-global-prep-cmd"s); + auto output = app_node.get_optional("output"s); + auto name = parse_env_val(this_env, app_node.get("name"s)); + auto cmd = app_node.get_optional("cmd"s); + auto image_path = app_node.get_optional("image-path"s); + auto working_dir = app_node.get_optional("working-dir"s); + + std::vector prep_cmds; + if (!exclude_global_prep.value_or(false)) { + prep_cmds.reserve(config::sunshine.prep_cmds.size()); + for (auto &prep_cmd : config::sunshine.prep_cmds) { + auto do_cmd = parse_env_val(this_env, prep_cmd.do_cmd); + auto undo_cmd = parse_env_val(this_env, prep_cmd.undo_cmd); + + prep_cmds.emplace_back(std::move(do_cmd), std::move(undo_cmd)); + } + } + + if (prep_nodes_opt) { + auto &prep_nodes = *prep_nodes_opt; + + prep_cmds.reserve(prep_cmds.size() + prep_nodes.size()); + for (auto &[_, prep_node] : prep_nodes) { + auto do_cmd = parse_env_val(this_env, prep_node.get("do"s)); + auto undo_cmd = prep_node.get_optional("undo"s); + + if (undo_cmd) { + prep_cmds.emplace_back(std::move(do_cmd), parse_env_val(this_env, *undo_cmd)); + } + else { + prep_cmds.emplace_back(std::move(do_cmd)); + } + } + } + + std::vector detached; + if (detached_nodes_opt) { + auto &detached_nodes = *detached_nodes_opt; + + detached.reserve(detached_nodes.size()); + for (auto &[_, detached_val] : detached_nodes) { + detached.emplace_back(parse_env_val(this_env, detached_val.get_value())); + } + } + + if (output) { + ctx.output = parse_env_val(this_env, *output); + } + + if (cmd) { + ctx.cmd = parse_env_val(this_env, *cmd); + } + + if (working_dir) { + ctx.working_dir = parse_env_val(this_env, *working_dir); + } + + if (image_path) { + ctx.image_path = parse_env_val(this_env, *image_path); + } + + auto possible_ids = calculate_app_id(name, ctx.image_path, i++); + if (ids.count(std::get<0>(possible_ids)) == 0) { + // Avoid using index to generate id if possible + ctx.id = std::get<0>(possible_ids); + } + else { + // Fallback to include index on collision + ctx.id = std::get<1>(possible_ids); + } + ids.insert(ctx.id); + + ctx.name = std::move(name); + ctx.prep_cmds = std::move(prep_cmds); + ctx.detached = std::move(detached); + + apps.emplace_back(std::move(ctx)); + } + + return proc::proc_t { + std::move(this_env), std::move(apps) + }; + } + catch (std::exception &e) { + BOOST_LOG(error) << e.what(); + } + + return std::nullopt; + } + + void + refresh(const std::string &file_name) { + auto proc_opt = proc::parse(file_name); + + if (proc_opt) { + proc = std::move(*proc_opt); + } + } +} // namespace proc diff --git a/src/process.h b/src/process.h index eab67779..12f4b5d9 100644 --- a/src/process.h +++ b/src/process.h @@ -4,7 +4,7 @@ #define SUNSHINE_PROCESS_H #ifndef __kernel_entry -#define __kernel_entry + #define __kernel_entry #endif #include @@ -16,10 +16,10 @@ #include "utility.h" namespace proc { -using file_t = util::safe_ptr_v2; + using file_t = util::safe_ptr_v2; -typedef config::prep_cmd_t cmd_t; -/* + typedef config::prep_cmd_t cmd_t; + /* * pre_cmds -- guaranteed to be executed unless any of the commands fail. * detached -- commands detached from Sunshine * cmd -- Runs indefinitely until: @@ -31,78 +31,89 @@ typedef config::prep_cmd_t cmd_t; * "null" -- The output of the commands are discarded * filename -- The output of the commands are appended to filename */ -struct ctx_t { - std::vector prep_cmds; + struct ctx_t { + std::vector prep_cmds; - /** + /** * Some applications, such as Steam, * either exit quickly, or keep running indefinitely. * Steam.exe is one such application. * That is why some applications need be run and forgotten about */ - std::vector detached; + std::vector detached; - std::string name; - std::string cmd; - std::string working_dir; - std::string output; - std::string image_path; - std::string id; -}; + std::string name; + std::string cmd; + std::string working_dir; + std::string output; + std::string image_path; + std::string id; + }; -class proc_t { -public: - KITTY_DEFAULT_CONSTR_MOVE_THROW(proc_t) + class proc_t { + public: + KITTY_DEFAULT_CONSTR_MOVE_THROW(proc_t) - proc_t( - boost::process::environment &&env, - std::vector &&apps) : _app_id(0), - _env(std::move(env)), - _apps(std::move(apps)) {} + proc_t( + boost::process::environment &&env, + std::vector &&apps): + _app_id(0), + _env(std::move(env)), + _apps(std::move(apps)) {} - int execute(int app_id); + int + execute(int app_id); - /** + /** * @return _app_id if a process is running, otherwise returns 0 */ - int running(); + int + running(); - ~proc_t(); + ~proc_t(); - const std::vector &get_apps() const; - std::vector &get_apps(); - std::string get_app_image(int app_id); + const std::vector & + get_apps() const; + std::vector & + get_apps(); + std::string + get_app_image(int app_id); - void terminate(); + void + terminate(); -private: - int _app_id; + private: + int _app_id; - boost::process::environment _env; - std::vector _apps; - ctx_t _app; + boost::process::environment _env; + std::vector _apps; + ctx_t _app; - // If no command associated with _app_id, yet it's still running - bool placebo {}; + // If no command associated with _app_id, yet it's still running + bool placebo {}; - boost::process::child _process; - boost::process::group _process_handle; + boost::process::child _process; + boost::process::group _process_handle; - file_t _pipe; - std::vector::const_iterator _app_prep_it; - std::vector::const_iterator _app_prep_begin; -}; + file_t _pipe; + std::vector::const_iterator _app_prep_it; + std::vector::const_iterator _app_prep_begin; + }; -/** + /** * Calculate a stable id based on name and image data * @return tuple of id calculated without index (for use if no collision) and one with */ -std::tuple calculate_app_id(const std::string &app_name, std::string app_image_path, int index); + std::tuple + calculate_app_id(const std::string &app_name, std::string app_image_path, int index); -std::string validate_app_image_path(std::string app_image_path); -void refresh(const std::string &file_name); -std::optional parse(const std::string &file_name); + std::string + validate_app_image_path(std::string app_image_path); + void + refresh(const std::string &file_name); + std::optional + parse(const std::string &file_name); -extern proc_t proc; -} // namespace proc -#endif // SUNSHINE_PROCESS_H + extern proc_t proc; +} // namespace proc +#endif // SUNSHINE_PROCESS_H diff --git a/src/round_robin.h b/src/round_robin.h index 09c32094..37e01033 100644 --- a/src/round_robin.h +++ b/src/round_robin.h @@ -4,154 +4,181 @@ #include namespace round_robin_util { -template -class it_wrap_t { -public: - using iterator_category = std::random_access_iterator_tag; - using value_type = V; - using difference_type = V; - using pointer = V *; - using reference = V &; + template + class it_wrap_t { + public: + using iterator_category = std::random_access_iterator_tag; + using value_type = V; + using difference_type = V; + using pointer = V *; + using reference = V &; - typedef T iterator; - typedef std::ptrdiff_t diff_t; + typedef T iterator; + typedef std::ptrdiff_t diff_t; + + iterator + operator+=(diff_t step) { + while (step-- > 0) { + ++_this(); + } + + return _this(); + } + + iterator + operator-=(diff_t step) { + while (step-- > 0) { + --_this(); + } + + return _this(); + } + + iterator + operator+(diff_t step) { + iterator new_ = _this(); + + return new_ += step; + } + + iterator + operator-(diff_t step) { + iterator new_ = _this(); + + return new_ -= step; + } + + diff_t + operator-(iterator first) { + diff_t step = 0; + while (first != _this()) { + ++step; + ++first; + } + + return step; + } + + iterator + operator++() { + _this().inc(); + return _this(); + } + iterator + operator--() { + _this().dec(); + return _this(); + } + + iterator + operator++(int) { + iterator new_ = _this(); - iterator operator+=(diff_t step) { - while(step-- > 0) { ++_this(); + + return new_; } - return _this(); - } + iterator + operator--(int) { + iterator new_ = _this(); - iterator operator-=(diff_t step) { - while(step-- > 0) { --_this(); + + return new_; } - return _this(); - } + reference + operator*() { return *_this().get(); } + const reference + operator*() const { return *_this().get(); } - iterator operator+(diff_t step) { - iterator new_ = _this(); + pointer + operator->() { return &*_this(); } + const pointer + operator->() const { return &*_this(); } - return new_ += step; - } - - iterator operator-(diff_t step) { - iterator new_ = _this(); - - return new_ -= step; - } - - diff_t operator-(iterator first) { - diff_t step = 0; - while(first != _this()) { - ++step; - ++first; + bool + operator!=(const iterator &other) const { + return !(_this() == other); } - return step; - } - - iterator operator++() { - _this().inc(); - return _this(); - } - iterator operator--() { - _this().dec(); - return _this(); - } - - iterator operator++(int) { - iterator new_ = _this(); - - ++_this(); - - return new_; - } - - iterator operator--(int) { - iterator new_ = _this(); - - --_this(); - - return new_; - } - - reference operator*() { return *_this().get(); } - const reference operator*() const { return *_this().get(); } - - pointer operator->() { return &*_this(); } - const pointer operator->() const { return &*_this(); } - - bool operator!=(const iterator &other) const { - return !(_this() == other); - } - - bool operator<(const iterator &other) const { - return !(_this() >= other); - } - - bool operator>=(const iterator &other) const { - return _this() == other || _this() > other; - } - - bool operator<=(const iterator &other) const { - return _this() == other || _this() < other; - } - - bool operator==(const iterator &other) const { return _this().eq(other); }; - bool operator>(const iterator &other) const { return _this().gt(other); } - -private: - iterator &_this() { return *static_cast(this); } - const iterator &_this() const { return *static_cast(this); } -}; - -template -class round_robin_t : public it_wrap_t> { -public: - using iterator = It; - using pointer = V *; - - round_robin_t(iterator begin, iterator end) : _begin(begin), _end(end), _pos(begin) {} - - void inc() { - ++_pos; - - if(_pos == _end) { - _pos = _begin; - } - } - - void dec() { - if(_pos == _begin) { - _pos = _end; + bool + operator<(const iterator &other) const { + return !(_this() >= other); } - --_pos; + bool + operator>=(const iterator &other) const { + return _this() == other || _this() > other; + } + + bool + operator<=(const iterator &other) const { + return _this() == other || _this() < other; + } + + bool + operator==(const iterator &other) const { return _this().eq(other); }; + bool + operator>(const iterator &other) const { return _this().gt(other); } + + private: + iterator & + _this() { return *static_cast(this); } + const iterator & + _this() const { return *static_cast(this); } + }; + + template + class round_robin_t: public it_wrap_t> { + public: + using iterator = It; + using pointer = V *; + + round_robin_t(iterator begin, iterator end): + _begin(begin), _end(end), _pos(begin) {} + + void + inc() { + ++_pos; + + if (_pos == _end) { + _pos = _begin; + } + } + + void + dec() { + if (_pos == _begin) { + _pos = _end; + } + + --_pos; + } + + bool + eq(const round_robin_t &other) const { + return *_pos == *other._pos; + } + + pointer + get() const { + return &*_pos; + } + + private: + It _begin; + It _end; + + It _pos; + }; + + template + round_robin_t + make_round_robin(It begin, It end) { + return round_robin_t(begin, end); } - - bool eq(const round_robin_t &other) const { - return *_pos == *other._pos; - } - - pointer get() const { - return &*_pos; - } - -private: - It _begin; - It _end; - - It _pos; -}; - -template -round_robin_t make_round_robin(It begin, It end) { - return round_robin_t(begin, end); -} -} // namespace round_robin_util +} // namespace round_robin_util #endif diff --git a/src/rtsp.cpp b/src/rtsp.cpp index 20c54c9f..98606be9 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -30,754 +30,784 @@ using asio::ip::udp; using namespace std::literals; namespace rtsp_stream { -void free_msg(PRTSP_MESSAGE msg) { - freeMessage(msg); + void + free_msg(PRTSP_MESSAGE msg) { + freeMessage(msg); - delete msg; -} - -class rtsp_server_t; - -using msg_t = util::safe_ptr; -using cmd_func_t = std::function; - -void print_msg(PRTSP_MESSAGE msg); -void cmd_not_found(tcp::socket &sock, msg_t &&req); -void respond(tcp::socket &sock, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload); - -class socket_t : public std::enable_shared_from_this { -public: - socket_t(boost::asio::io_service &ios, std::function &&handle_data_fn) - : handle_data_fn { std::move(handle_data_fn) }, sock { ios } {} - - void read() { - if(begin == std::end(msg_buf)) { - BOOST_LOG(error) << "RTSP: read(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); - - respond(sock, nullptr, 400, "BAD REQUEST", 0, {}); - - sock.close(); - - return; - } - - sock.async_read_some( - boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), - boost::bind( - &socket_t::handle_read, shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred)); + delete msg; } - void read_payload() { - if(begin == std::end(msg_buf)) { - BOOST_LOG(error) << "RTSP: read_payload(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); + class rtsp_server_t; - respond(sock, nullptr, 400, "BAD REQUEST", 0, {}); + using msg_t = util::safe_ptr; + using cmd_func_t = std::function; - sock.close(); + void + print_msg(PRTSP_MESSAGE msg); + void + cmd_not_found(tcp::socket &sock, msg_t &&req); + void + respond(tcp::socket &sock, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload); - return; - } + class socket_t: public std::enable_shared_from_this { + public: + socket_t(boost::asio::io_service &ios, std::function &&handle_data_fn): + handle_data_fn { std::move(handle_data_fn) }, sock { ios } {} - sock.async_read_some( - boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), - boost::bind( - &socket_t::handle_payload, shared_from_this(), - boost::asio::placeholders::error, - boost::asio::placeholders::bytes_transferred)); - } + void + read() { + if (begin == std::end(msg_buf)) { + BOOST_LOG(error) << "RTSP: read(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); - static void handle_payload(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { - BOOST_LOG(debug) << "handle_payload(): Handle read of size: "sv << bytes << " bytes"sv; + respond(sock, nullptr, 400, "BAD REQUEST", 0, {}); - auto sock_close = util::fail_guard([&socket]() { - boost::system::error_code ec; - socket->sock.close(ec); + sock.close(); - if(ec) { - BOOST_LOG(error) << "RTSP: handle_payload(): Couldn't close tcp socket: "sv << ec.message(); + return; } - }); - if(ec) { - BOOST_LOG(error) << "RTSP: handle_payload(): Couldn't read from tcp socket: "sv << ec.message(); - - return; + sock.async_read_some( + boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), + boost::bind( + &socket_t::handle_read, shared_from_this(), + boost::asio::placeholders::error, + boost::asio::placeholders::bytes_transferred)); } - auto end = socket->begin + bytes; - msg_t req { new msg_t::element_type {} }; - if(auto status = parseRtspMessage(req.get(), socket->msg_buf.data(), (std::size_t)(end - socket->msg_buf.data()))) { - BOOST_LOG(error) << "Malformed RTSP message: ["sv << status << ']'; + void + read_payload() { + if (begin == std::end(msg_buf)) { + BOOST_LOG(error) << "RTSP: read_payload(): Exceeded maximum rtsp packet size: "sv << msg_buf.size(); - respond(socket->sock, nullptr, 400, "BAD REQUEST", req->sequenceNumber, {}); - return; - } + respond(sock, nullptr, 400, "BAD REQUEST", 0, {}); - sock_close.disable(); + sock.close(); - auto fg = util::fail_guard([&socket]() { - socket->read_payload(); - }); - - auto content_length = 0; - for(auto option = req->options; option != nullptr; option = option->next) { - if("Content-length"sv == option->option) { - BOOST_LOG(debug) << "Found Content-Length: "sv << option->content << " bytes"sv; - - // If content_length > bytes read, then we need to store current data read, - // to be appended by the next read. - std::string_view content { option->content }; - auto begin = std::find_if(std::begin(content), std::end(content), [](auto ch) { return (bool)std::isdigit(ch); }); - - content_length = util::from_chars(begin, std::end(content)); - break; + return; } + + sock.async_read_some( + boost::asio::buffer(begin, (std::size_t)(std::end(msg_buf) - begin)), + boost::bind( + &socket_t::handle_payload, shared_from_this(), + boost::asio::placeholders::error, + boost::asio::placeholders::bytes_transferred)); } - if(end - socket->crlf >= content_length) { - if(end - socket->crlf > content_length) { - BOOST_LOG(warning) << "(end - socket->crlf) > content_length -- "sv << (std::size_t)(end - socket->crlf) << " > "sv << content_length; + static void + handle_payload(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + BOOST_LOG(debug) << "handle_payload(): Handle read of size: "sv << bytes << " bytes"sv; + + auto sock_close = util::fail_guard([&socket]() { + boost::system::error_code ec; + socket->sock.close(ec); + + if (ec) { + BOOST_LOG(error) << "RTSP: handle_payload(): Couldn't close tcp socket: "sv << ec.message(); + } + }); + + if (ec) { + BOOST_LOG(error) << "RTSP: handle_payload(): Couldn't read from tcp socket: "sv << ec.message(); + + return; } + auto end = socket->begin + bytes; + msg_t req { new msg_t::element_type {} }; + if (auto status = parseRtspMessage(req.get(), socket->msg_buf.data(), (std::size_t)(end - socket->msg_buf.data()))) { + BOOST_LOG(error) << "Malformed RTSP message: ["sv << status << ']'; + + respond(socket->sock, nullptr, 400, "BAD REQUEST", req->sequenceNumber, {}); + return; + } + + sock_close.disable(); + + auto fg = util::fail_guard([&socket]() { + socket->read_payload(); + }); + + auto content_length = 0; + for (auto option = req->options; option != nullptr; option = option->next) { + if ("Content-length"sv == option->option) { + BOOST_LOG(debug) << "Found Content-Length: "sv << option->content << " bytes"sv; + + // If content_length > bytes read, then we need to store current data read, + // to be appended by the next read. + std::string_view content { option->content }; + auto begin = std::find_if(std::begin(content), std::end(content), [](auto ch) { return (bool) std::isdigit(ch); }); + + content_length = util::from_chars(begin, std::end(content)); + break; + } + } + + if (end - socket->crlf >= content_length) { + if (end - socket->crlf > content_length) { + BOOST_LOG(warning) << "(end - socket->crlf) > content_length -- "sv << (std::size_t)(end - socket->crlf) << " > "sv << content_length; + } + + fg.disable(); + print_msg(req.get()); + + socket->handle_data(std::move(req)); + } + + socket->begin = end; + } + + static void + handle_read(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { + BOOST_LOG(debug) << "handle_read(): Handle read of size: "sv << bytes << " bytes"sv; + + if (ec) { + BOOST_LOG(error) << "RTSP: handle_read(): Couldn't read from tcp socket: "sv << ec.message(); + + boost::system::error_code ec; + socket->sock.close(ec); + + if (ec) { + BOOST_LOG(error) << "RTSP: handle_read(): Couldn't close tcp socket: "sv << ec.message(); + } + + return; + } + + auto fg = util::fail_guard([&socket]() { + socket->read(); + }); + + auto begin = std::max(socket->begin - 4, socket->begin); + auto buf_size = bytes + (begin - socket->begin); + auto end = begin + buf_size; + + constexpr auto needle = "\r\n\r\n"sv; + + auto it = std::search(begin, begin + buf_size, std::begin(needle), std::end(needle)); + if (it == end) { + socket->begin = end; + + return; + } + + // Emulate read completion for payload data + socket->begin = it + needle.size(); + socket->crlf = socket->begin; + buf_size = end - socket->begin; + fg.disable(); - print_msg(req.get()); - - socket->handle_data(std::move(req)); + handle_payload(socket, ec, buf_size); } - socket->begin = end; - } + void + handle_data(msg_t &&req) { + handle_data_fn(sock, std::move(req)); + } - static void handle_read(std::shared_ptr &socket, const boost::system::error_code &ec, std::size_t bytes) { - BOOST_LOG(debug) << "handle_read(): Handle read of size: "sv << bytes << " bytes"sv; + std::function handle_data_fn; - if(ec) { - BOOST_LOG(error) << "RTSP: handle_read(): Couldn't read from tcp socket: "sv << ec.message(); + tcp::socket sock; - boost::system::error_code ec; - socket->sock.close(ec); + std::array msg_buf; - if(ec) { - BOOST_LOG(error) << "RTSP: handle_read(): Couldn't close tcp socket: "sv << ec.message(); + char *crlf; + char *begin = msg_buf.data(); + }; + + class rtsp_server_t { + public: + ~rtsp_server_t() { + clear(); + } + + int + bind(std::uint16_t port, boost::system::error_code &ec) { + { + auto lg = _session_slots.lock(); + + _session_slots->resize(config::stream.channels); + _slot_count = config::stream.channels; } - return; + acceptor.open(tcp::v4(), ec); + if (ec) { + return -1; + } + + acceptor.set_option(boost::asio::socket_base::reuse_address { true }); + + acceptor.bind(tcp::endpoint(tcp::v4(), port), ec); + if (ec) { + return -1; + } + + acceptor.listen(4096, ec); + if (ec) { + return -1; + } + + next_socket = std::make_shared(ios, [this](tcp::socket &sock, msg_t &&msg) { + handle_msg(sock, std::move(msg)); + }); + + acceptor.async_accept(next_socket->sock, [this](const auto &ec) { + handle_accept(ec); + }); + + return 0; } - auto fg = util::fail_guard([&socket]() { + template + void + iterate(std::chrono::duration timeout) { + ios.run_one_for(timeout); + } + + void + handle_msg(tcp::socket &sock, msg_t &&req) { + auto func = _map_cmd_cb.find(req->message.request.command); + if (func != std::end(_map_cmd_cb)) { + func->second(this, sock, std::move(req)); + } + else { + cmd_not_found(sock, std::move(req)); + } + + sock.shutdown(boost::asio::socket_base::shutdown_type::shutdown_both); + } + + void + handle_accept(const boost::system::error_code &ec) { + if (ec) { + BOOST_LOG(error) << "Couldn't accept incoming connections: "sv << ec.message(); + + // Stop server + clear(); + return; + } + + auto socket = std::move(next_socket); socket->read(); - }); - auto begin = std::max(socket->begin - 4, socket->begin); - auto buf_size = bytes + (begin - socket->begin); - auto end = begin + buf_size; + next_socket = std::make_shared(ios, [this](tcp::socket &sock, msg_t &&msg) { + handle_msg(sock, std::move(msg)); + }); - constexpr auto needle = "\r\n\r\n"sv; - - auto it = std::search(begin, begin + buf_size, std::begin(needle), std::end(needle)); - if(it == end) { - socket->begin = end; - - return; + acceptor.async_accept(next_socket->sock, [this](const auto &ec) { + handle_accept(ec); + }); } - // Emulate read completion for payload data - socket->begin = it + needle.size(); - socket->crlf = socket->begin; - buf_size = end - socket->begin; + void + map(const std::string_view &type, cmd_func_t cb) { + _map_cmd_cb.emplace(type, std::move(cb)); + } - fg.disable(); - handle_payload(socket, ec, buf_size); - } + void + session_raise(rtsp_stream::launch_session_t launch_session) { + auto now = std::chrono::steady_clock::now(); - void handle_data(msg_t &&req) { - handle_data_fn(sock, std::move(req)); - } + // If a launch event is still pending, don't overwrite it. + if (raised_timeout > now && launch_event.peek()) { + return; + } + raised_timeout = now + 10s; - std::function handle_data_fn; + --_slot_count; + launch_event.raise(launch_session); + } - tcp::socket sock; + int + session_count() const { + return config::stream.channels - _slot_count; + } - std::array msg_buf; + safe::event_t launch_event; - char *crlf; - char *begin = msg_buf.data(); -}; + void + clear(bool all = true) { + // if a launch event timed out --> Remove it. + if (raised_timeout < std::chrono::steady_clock::now()) { + auto discarded = launch_event.pop(0s); + if (discarded) { + ++_slot_count; + } + } -class rtsp_server_t { -public: - ~rtsp_server_t() { - clear(); - } - - int bind(std::uint16_t port, boost::system::error_code &ec) { - { auto lg = _session_slots.lock(); - _session_slots->resize(config::stream.channels); - _slot_count = config::stream.channels; + for (auto &slot : *_session_slots) { + if (slot && (all || stream::session::state(*slot) == stream::session::state_e::STOPPING)) { + stream::session::stop(*slot); + stream::session::join(*slot); + + slot.reset(); + + ++_slot_count; + } + } + + if (all && !ios.stopped()) { + ios.stop(); + } } - acceptor.open(tcp::v4(), ec); - if(ec) { - return -1; + void + clear(std::shared_ptr *session_p) { + auto lg = _session_slots.lock(); + + session_p->reset(); + + ++_slot_count; } - acceptor.set_option(boost::asio::socket_base::reuse_address { true }); + std::shared_ptr * + accept(std::shared_ptr &session) { + auto lg = _session_slots.lock(); - acceptor.bind(tcp::endpoint(tcp::v4(), port), ec); - if(ec) { - return -1; + for (auto &slot : *_session_slots) { + if (!slot) { + slot = session; + return &slot; + } + } + + return nullptr; } - acceptor.listen(4096, ec); - if(ec) { - return -1; + private: + std::unordered_map _map_cmd_cb; + + sync_util::sync_t>> _session_slots; + + std::chrono::steady_clock::time_point raised_timeout; + int _slot_count; + + boost::asio::io_service ios; + tcp::acceptor acceptor { ios }; + + std::shared_ptr next_socket; + }; + + rtsp_server_t server {}; + + void + launch_session_raise(rtsp_stream::launch_session_t launch_session) { + server.session_raise(launch_session); + } + + int + session_count() { + // Ensure session_count is up-to-date + server.clear(false); + + return server.session_count(); + } + + int + send(tcp::socket &sock, const std::string_view &sv) { + std::size_t bytes_send = 0; + + while (bytes_send != sv.size()) { + boost::system::error_code ec; + bytes_send += sock.send(boost::asio::buffer(sv.substr(bytes_send)), 0, ec); + + if (ec) { + BOOST_LOG(error) << "RTSP: Couldn't send data over tcp socket: "sv << ec.message(); + return -1; + } } - next_socket = std::make_shared(ios, [this](tcp::socket &sock, msg_t &&msg) { - handle_msg(sock, std::move(msg)); - }); - - acceptor.async_accept(next_socket->sock, [this](const auto &ec) { - handle_accept(ec); - }); - return 0; } - template - void iterate(std::chrono::duration timeout) { - ios.run_one_for(timeout); - } + void + respond(tcp::socket &sock, msg_t &resp) { + auto payload = std::make_pair(resp->payload, resp->payloadLength); - void handle_msg(tcp::socket &sock, msg_t &&req) { - auto func = _map_cmd_cb.find(req->message.request.command); - if(func != std::end(_map_cmd_cb)) { - func->second(this, sock, std::move(req)); - } - else { - cmd_not_found(sock, std::move(req)); - } + // Restore response message for proper destruction + auto lg = util::fail_guard([&]() { + resp->payload = payload.first; + resp->payloadLength = payload.second; + }); - sock.shutdown(boost::asio::socket_base::shutdown_type::shutdown_both); - } + resp->payload = nullptr; + resp->payloadLength = 0; - void handle_accept(const boost::system::error_code &ec) { - if(ec) { - BOOST_LOG(error) << "Couldn't accept incoming connections: "sv << ec.message(); + int serialized_len; + util::c_ptr raw_resp { serializeRtspMessage(resp.get(), &serialized_len) }; + BOOST_LOG(debug) + << "---Begin Response---"sv << std::endl + << std::string_view { raw_resp.get(), (std::size_t) serialized_len } << std::endl + << std::string_view { payload.first, (std::size_t) payload.second } << std::endl + << "---End Response---"sv << std::endl; - // Stop server - clear(); + std::string_view tmp_resp { raw_resp.get(), (size_t) serialized_len }; + + if (send(sock, tmp_resp)) { return; } - auto socket = std::move(next_socket); - socket->read(); - - next_socket = std::make_shared(ios, [this](tcp::socket &sock, msg_t &&msg) { - handle_msg(sock, std::move(msg)); - }); - - acceptor.async_accept(next_socket->sock, [this](const auto &ec) { - handle_accept(ec); - }); + send(sock, std::string_view { payload.first, (std::size_t) payload.second }); } - void map(const std::string_view &type, cmd_func_t cb) { - _map_cmd_cb.emplace(type, std::move(cb)); + void + respond(tcp::socket &sock, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) { + msg_t resp { new msg_t::element_type }; + createRtspResponse(resp.get(), nullptr, 0, const_cast("RTSP/1.0"), statuscode, const_cast(status_msg), seqn, options, const_cast(payload.data()), (int) payload.size()); + + respond(sock, resp); } - void session_raise(rtsp_stream::launch_session_t launch_session) { - auto now = std::chrono::steady_clock::now(); - - // If a launch event is still pending, don't overwrite it. - if(raised_timeout > now && launch_event.peek()) { - return; - } - raised_timeout = now + 10s; - - --_slot_count; - launch_event.raise(launch_session); + void + cmd_not_found(tcp::socket &sock, msg_t &&req) { + respond(sock, nullptr, 404, "NOT FOUND", req->sequenceNumber, {}); } - int session_count() const { - return config::stream.channels - _slot_count; + void + cmd_option(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { + OPTION_ITEM option {}; + + // I know these string literals will not be modified + option.option = const_cast("CSeq"); + + auto seqn_str = std::to_string(req->sequenceNumber); + option.content = const_cast(seqn_str.c_str()); + + respond(sock, &option, 200, "OK", req->sequenceNumber, {}); } - safe::event_t launch_event; + void + cmd_describe(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { + OPTION_ITEM option {}; - void clear(bool all = true) { - // if a launch event timed out --> Remove it. - if(raised_timeout < std::chrono::steady_clock::now()) { - auto discarded = launch_event.pop(0s); - if(discarded) { - ++_slot_count; - } + // I know these string literals will not be modified + option.option = const_cast("CSeq"); + + auto seqn_str = std::to_string(req->sequenceNumber); + option.content = const_cast(seqn_str.c_str()); + + std::stringstream ss; + if (config::video.hevc_mode != 1) { + ss << "sprop-parameter-sets=AAAAAU"sv << std::endl; } - auto lg = _session_slots.lock(); + for (int x = 0; x < audio::MAX_STREAM_CONFIG; ++x) { + auto &stream_config = audio::stream_configs[x]; + std::uint8_t mapping[platf::speaker::MAX_SPEAKERS]; - for(auto &slot : *_session_slots) { - if(slot && (all || stream::session::state(*slot) == stream::session::state_e::STOPPING)) { - stream::session::stop(*slot); - stream::session::join(*slot); + auto mapping_p = stream_config.mapping; - slot.reset(); - - ++_slot_count; - } - } - - if(all && !ios.stopped()) { - ios.stop(); - } - } - - void clear(std::shared_ptr *session_p) { - auto lg = _session_slots.lock(); - - session_p->reset(); - - ++_slot_count; - } - - std::shared_ptr *accept(std::shared_ptr &session) { - auto lg = _session_slots.lock(); - - for(auto &slot : *_session_slots) { - if(!slot) { - slot = session; - return &slot; - } - } - - return nullptr; - } - -private: - std::unordered_map _map_cmd_cb; - - sync_util::sync_t>> _session_slots; - - std::chrono::steady_clock::time_point raised_timeout; - int _slot_count; - - boost::asio::io_service ios; - tcp::acceptor acceptor { ios }; - - std::shared_ptr next_socket; -}; - -rtsp_server_t server {}; - -void launch_session_raise(rtsp_stream::launch_session_t launch_session) { - server.session_raise(launch_session); -} - -int session_count() { - // Ensure session_count is up-to-date - server.clear(false); - - return server.session_count(); -} - -int send(tcp::socket &sock, const std::string_view &sv) { - std::size_t bytes_send = 0; - - while(bytes_send != sv.size()) { - boost::system::error_code ec; - bytes_send += sock.send(boost::asio::buffer(sv.substr(bytes_send)), 0, ec); - - if(ec) { - BOOST_LOG(error) << "RTSP: Couldn't send data over tcp socket: "sv << ec.message(); - return -1; - } - } - - return 0; -} - -void respond(tcp::socket &sock, msg_t &resp) { - auto payload = std::make_pair(resp->payload, resp->payloadLength); - - // Restore response message for proper destruction - auto lg = util::fail_guard([&]() { - resp->payload = payload.first; - resp->payloadLength = payload.second; - }); - - resp->payload = nullptr; - resp->payloadLength = 0; - - int serialized_len; - util::c_ptr raw_resp { serializeRtspMessage(resp.get(), &serialized_len) }; - BOOST_LOG(debug) - << "---Begin Response---"sv << std::endl - << std::string_view { raw_resp.get(), (std::size_t)serialized_len } << std::endl - << std::string_view { payload.first, (std::size_t)payload.second } << std::endl - << "---End Response---"sv << std::endl; - - std::string_view tmp_resp { raw_resp.get(), (size_t)serialized_len }; - - if(send(sock, tmp_resp)) { - return; - } - - send(sock, std::string_view { payload.first, (std::size_t)payload.second }); -} - -void respond(tcp::socket &sock, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) { - msg_t resp { new msg_t::element_type }; - createRtspResponse(resp.get(), nullptr, 0, const_cast("RTSP/1.0"), statuscode, const_cast(status_msg), seqn, options, const_cast(payload.data()), (int)payload.size()); - - respond(sock, resp); -} - -void cmd_not_found(tcp::socket &sock, msg_t &&req) { - respond(sock, nullptr, 404, "NOT FOUND", req->sequenceNumber, {}); -} - -void cmd_option(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { - OPTION_ITEM option {}; - - // I know these string literals will not be modified - option.option = const_cast("CSeq"); - - auto seqn_str = std::to_string(req->sequenceNumber); - option.content = const_cast(seqn_str.c_str()); - - respond(sock, &option, 200, "OK", req->sequenceNumber, {}); -} - -void cmd_describe(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { - OPTION_ITEM option {}; - - // I know these string literals will not be modified - option.option = const_cast("CSeq"); - - auto seqn_str = std::to_string(req->sequenceNumber); - option.content = const_cast(seqn_str.c_str()); - - std::stringstream ss; - if(config::video.hevc_mode != 1) { - ss << "sprop-parameter-sets=AAAAAU"sv << std::endl; - } - - for(int x = 0; x < audio::MAX_STREAM_CONFIG; ++x) { - auto &stream_config = audio::stream_configs[x]; - std::uint8_t mapping[platf::speaker::MAX_SPEAKERS]; - - auto mapping_p = stream_config.mapping; - - /** + /** * GFE advertises incorrect mapping for normal quality configurations, * as a result, Moonlight rotates all channels from index '3' to the right * To work around this, rotate channels to the left from index '3' */ - if(x == audio::SURROUND51 || x == audio::SURROUND71) { - std::copy_n(mapping_p, stream_config.channelCount, mapping); - std::rotate(mapping + 3, mapping + 4, mapping + audio::MAX_STREAM_CONFIG); + if (x == audio::SURROUND51 || x == audio::SURROUND71) { + std::copy_n(mapping_p, stream_config.channelCount, mapping); + std::rotate(mapping + 3, mapping + 4, mapping + audio::MAX_STREAM_CONFIG); - mapping_p = mapping; - } - - ss << "a=fmtp:97 surround-params="sv << stream_config.channelCount << stream_config.streams << stream_config.coupledStreams; - - std::for_each_n(mapping_p, stream_config.channelCount, [&ss](std::uint8_t digit) { - ss << (char)(digit + '0'); - }); - - ss << std::endl; - } - - respond(sock, &option, 200, "OK", req->sequenceNumber, ss.str()); -} - -void cmd_setup(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { - OPTION_ITEM options[3] {}; - - auto &seqn = options[0]; - auto &session_option = options[1]; - auto &port_option = options[2]; - - seqn.option = const_cast("CSeq"); - - auto seqn_str = std::to_string(req->sequenceNumber); - seqn.content = const_cast(seqn_str.c_str()); - - std::string_view target { req->message.request.target }; - auto begin = std::find(std::begin(target), std::end(target), '=') + 1; - auto end = std::find(begin, std::end(target), '/'); - std::string_view type { begin, (size_t)std::distance(begin, end) }; - - std::uint16_t port; - if(type == "audio"sv) { - port = map_port(stream::AUDIO_STREAM_PORT); - } - else if(type == "video"sv) { - port = map_port(stream::VIDEO_STREAM_PORT); - } - else if(type == "control"sv) { - port = map_port(stream::CONTROL_PORT); - } - else { - cmd_not_found(sock, std::move(req)); - - return; - } - - seqn.next = &session_option; - - session_option.option = const_cast("Session"); - session_option.content = const_cast("DEADBEEFCAFE;timeout = 90"); - - session_option.next = &port_option; - - // Moonlight merely requires 'server_port=' - auto port_value = "server_port=" + std::to_string(port); - - port_option.option = const_cast("Transport"); - port_option.content = port_value.data(); - - - respond(sock, &seqn, 200, "OK", req->sequenceNumber, {}); -} - -void cmd_announce(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { - OPTION_ITEM option {}; - - // I know these string literals will not be modified - option.option = const_cast("CSeq"); - - auto seqn_str = std::to_string(req->sequenceNumber); - option.content = const_cast(seqn_str.c_str()); - - if(!server->launch_event.peek()) { - // /launch has not been used - - respond(sock, &option, 503, "Service Unavailable", req->sequenceNumber, {}); - return; - } - auto launch_session { server->launch_event.pop() }; - - std::string_view payload { req->payload, (size_t)req->payloadLength }; - - std::vector lines; - - auto whitespace = [](char ch) { - return ch == '\n' || ch == '\r'; - }; - - { - auto pos = std::begin(payload); - auto begin = pos; - while(pos != std::end(payload)) { - if(whitespace(*pos++)) { - lines.emplace_back(begin, pos - begin - 1); - - while(pos != std::end(payload) && whitespace(*pos)) { ++pos; } - begin = pos; + mapping_p = mapping; } + + ss << "a=fmtp:97 surround-params="sv << stream_config.channelCount << stream_config.streams << stream_config.coupledStreams; + + std::for_each_n(mapping_p, stream_config.channelCount, [&ss](std::uint8_t digit) { + ss << (char) (digit + '0'); + }); + + ss << std::endl; } + + respond(sock, &option, 200, "OK", req->sequenceNumber, ss.str()); } - std::string_view client; - std::unordered_map args; + void + cmd_setup(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { + OPTION_ITEM options[3] {}; - for(auto line : lines) { - auto type = line.substr(0, 2); - if(type == "s="sv) { - client = line.substr(2); + auto &seqn = options[0]; + auto &session_option = options[1]; + auto &port_option = options[2]; + + seqn.option = const_cast("CSeq"); + + auto seqn_str = std::to_string(req->sequenceNumber); + seqn.content = const_cast(seqn_str.c_str()); + + std::string_view target { req->message.request.target }; + auto begin = std::find(std::begin(target), std::end(target), '=') + 1; + auto end = std::find(begin, std::end(target), '/'); + std::string_view type { begin, (size_t) std::distance(begin, end) }; + + std::uint16_t port; + if (type == "audio"sv) { + port = map_port(stream::AUDIO_STREAM_PORT); } - else if(type == "a=") { - auto pos = line.find(':'); - - auto name = line.substr(2, pos - 2); - auto val = line.substr(pos + 1); - - if(val[val.size() - 1] == ' ') { - val = val.substr(0, val.size() - 1); - } - args.emplace(name, val); + else if (type == "video"sv) { + port = map_port(stream::VIDEO_STREAM_PORT); } - } - - // Initialize any omitted parameters to defaults - args.try_emplace("x-nv-video[0].encoderCscMode"sv, "0"sv); - args.try_emplace("x-nv-vqos[0].bitStreamFormat"sv, "0"sv); - args.try_emplace("x-nv-video[0].dynamicRangeMode"sv, "0"sv); - args.try_emplace("x-nv-aqos.packetDuration"sv, "5"sv); - args.try_emplace("x-nv-general.useReliableUdp"sv, "1"sv); - args.try_emplace("x-nv-vqos[0].fec.minRequiredFecPackets"sv, "0"sv); - args.try_emplace("x-nv-general.featureFlags"sv, "135"sv); - args.try_emplace("x-nv-vqos[0].qosTrafficType"sv, "5"sv); - args.try_emplace("x-nv-aqos.qosTrafficType"sv, "4"sv); - - stream::config_t config; - - config.audio.flags[audio::config_t::HOST_AUDIO] = launch_session->host_audio; - try { - config.audio.channels = util::from_view(args.at("x-nv-audio.surround.numChannels"sv)); - config.audio.mask = util::from_view(args.at("x-nv-audio.surround.channelMask"sv)); - config.audio.packetDuration = util::from_view(args.at("x-nv-aqos.packetDuration"sv)); - - config.audio.flags[audio::config_t::HIGH_QUALITY] = - util::from_view(args.at("x-nv-audio.surround.AudioQuality"sv)); - - config.controlProtocolType = util::from_view(args.at("x-nv-general.useReliableUdp"sv)); - config.packetsize = util::from_view(args.at("x-nv-video[0].packetSize"sv)); - config.minRequiredFecPackets = util::from_view(args.at("x-nv-vqos[0].fec.minRequiredFecPackets"sv)); - config.featureFlags = util::from_view(args.at("x-nv-general.featureFlags"sv)); - config.audioQosType = util::from_view(args.at("x-nv-aqos.qosTrafficType"sv)); - config.videoQosType = util::from_view(args.at("x-nv-vqos[0].qosTrafficType"sv)); - - config.monitor.height = util::from_view(args.at("x-nv-video[0].clientViewportHt"sv)); - config.monitor.width = util::from_view(args.at("x-nv-video[0].clientViewportWd"sv)); - config.monitor.framerate = util::from_view(args.at("x-nv-video[0].maxFPS"sv)); - config.monitor.bitrate = util::from_view(args.at("x-nv-vqos[0].bw.maximumBitrateKbps"sv)); - config.monitor.slicesPerFrame = util::from_view(args.at("x-nv-video[0].videoEncoderSlicesPerFrame"sv)); - config.monitor.numRefFrames = util::from_view(args.at("x-nv-video[0].maxNumReferenceFrames"sv)); - config.monitor.encoderCscMode = util::from_view(args.at("x-nv-video[0].encoderCscMode"sv)); - config.monitor.videoFormat = util::from_view(args.at("x-nv-vqos[0].bitStreamFormat"sv)); - config.monitor.dynamicRange = util::from_view(args.at("x-nv-video[0].dynamicRangeMode"sv)); - } - catch(std::out_of_range &) { - - respond(sock, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); - return; - } - - // When using stereo audio, the audio quality is (strangely) indicated by whether the Host field - // in the RTSP message matches a local interface's IP address. Fortunately, Moonlight always sends - // 0.0.0.0 when it wants low quality, so it is easy to check without enumerating interfaces. - if(config.audio.channels == 2) { - for(auto option = req->options; option != nullptr; option = option->next) { - if("Host"sv == option->option) { - std::string_view content { option->content }; - BOOST_LOG(debug) << "Found Host: "sv << content; - config.audio.flags[audio::config_t::HIGH_QUALITY] = (content.find("0.0.0.0"sv) == std::string::npos); - } - } - } - - if(config.monitor.videoFormat != 0 && config::video.hevc_mode == 1) { - BOOST_LOG(warning) << "HEVC is disabled, yet the client requested HEVC"sv; - - respond(sock, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); - return; - } - - auto session = stream::session::alloc(config, launch_session->gcm_key, launch_session->iv); - - auto slot = server->accept(session); - if(!slot) { - BOOST_LOG(info) << "Ran out of slots for client from ["sv << ']'; - - respond(sock, &option, 503, "Service Unavailable", req->sequenceNumber, {}); - return; - } - - if(stream::session::start(*session, sock.remote_endpoint().address().to_string())) { - BOOST_LOG(error) << "Failed to start a streaming session"sv; - - server->clear(slot); - respond(sock, &option, 500, "Internal Server Error", req->sequenceNumber, {}); - return; - } - - respond(sock, &option, 200, "OK", req->sequenceNumber, {}); -} - -void cmd_play(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { - OPTION_ITEM option {}; - - // I know these string literals will not be modified - option.option = const_cast("CSeq"); - - auto seqn_str = std::to_string(req->sequenceNumber); - option.content = const_cast(seqn_str.c_str()); - - respond(sock, &option, 200, "OK", req->sequenceNumber, {}); -} - -void rtpThread() { - auto shutdown_event = mail::man->event(mail::shutdown); - auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); - - server.map("OPTIONS"sv, &cmd_option); - server.map("DESCRIBE"sv, &cmd_describe); - server.map("SETUP"sv, &cmd_setup); - server.map("ANNOUNCE"sv, &cmd_announce); - - server.map("PLAY"sv, &cmd_play); - - boost::system::error_code ec; - if(server.bind(map_port(rtsp_stream::RTSP_SETUP_PORT), ec)) { - BOOST_LOG(fatal) << "Couldn't bind RTSP server to port ["sv << map_port(rtsp_stream::RTSP_SETUP_PORT) << "], " << ec.message(); - shutdown_event->raise(true); - - return; - } - - while(!shutdown_event->peek()) { - server.iterate(std::min(500ms, config::stream.ping_timeout)); - - if(broadcast_shutdown_event->peek()) { - server.clear(); + else if (type == "control"sv) { + port = map_port(stream::CONTROL_PORT); } else { - // cleanup all stopped sessions - server.clear(false); + cmd_not_found(sock, std::move(req)); + + return; } + + seqn.next = &session_option; + + session_option.option = const_cast("Session"); + session_option.content = const_cast("DEADBEEFCAFE;timeout = 90"); + + session_option.next = &port_option; + + // Moonlight merely requires 'server_port=' + auto port_value = "server_port=" + std::to_string(port); + + port_option.option = const_cast("Transport"); + port_option.content = port_value.data(); + + respond(sock, &seqn, 200, "OK", req->sequenceNumber, {}); } - server.clear(); -} + void + cmd_announce(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { + OPTION_ITEM option {}; -void print_msg(PRTSP_MESSAGE msg) { - std::string_view type = msg->type == TYPE_RESPONSE ? "RESPONSE"sv : "REQUEST"sv; + // I know these string literals will not be modified + option.option = const_cast("CSeq"); - std::string_view payload { msg->payload, (size_t)msg->payloadLength }; - std::string_view protocol { msg->protocol }; - auto seqnm = msg->sequenceNumber; - std::string_view messageBuffer { msg->messageBuffer }; + auto seqn_str = std::to_string(req->sequenceNumber); + option.content = const_cast(seqn_str.c_str()); - BOOST_LOG(debug) << "type ["sv << type << ']'; - BOOST_LOG(debug) << "sequence number ["sv << seqnm << ']'; - BOOST_LOG(debug) << "protocol :: "sv << protocol; - BOOST_LOG(debug) << "payload :: "sv << payload; + if (!server->launch_event.peek()) { + // /launch has not been used - if(msg->type == TYPE_RESPONSE) { - auto &resp = msg->message.response; + respond(sock, &option, 503, "Service Unavailable", req->sequenceNumber, {}); + return; + } + auto launch_session { server->launch_event.pop() }; - auto statuscode = resp.statusCode; - std::string_view status { resp.statusString }; + std::string_view payload { req->payload, (size_t) req->payloadLength }; - BOOST_LOG(debug) << "statuscode :: "sv << statuscode; - BOOST_LOG(debug) << "status :: "sv << status; - } - else { - auto &req = msg->message.request; + std::vector lines; - std::string_view command { req.command }; - std::string_view target { req.target }; + auto whitespace = [](char ch) { + return ch == '\n' || ch == '\r'; + }; - BOOST_LOG(debug) << "command :: "sv << command; - BOOST_LOG(debug) << "target :: "sv << target; + { + auto pos = std::begin(payload); + auto begin = pos; + while (pos != std::end(payload)) { + if (whitespace(*pos++)) { + lines.emplace_back(begin, pos - begin - 1); + + while (pos != std::end(payload) && whitespace(*pos)) { ++pos; } + begin = pos; + } + } + } + + std::string_view client; + std::unordered_map args; + + for (auto line : lines) { + auto type = line.substr(0, 2); + if (type == "s="sv) { + client = line.substr(2); + } + else if (type == "a=") { + auto pos = line.find(':'); + + auto name = line.substr(2, pos - 2); + auto val = line.substr(pos + 1); + + if (val[val.size() - 1] == ' ') { + val = val.substr(0, val.size() - 1); + } + args.emplace(name, val); + } + } + + // Initialize any omitted parameters to defaults + args.try_emplace("x-nv-video[0].encoderCscMode"sv, "0"sv); + args.try_emplace("x-nv-vqos[0].bitStreamFormat"sv, "0"sv); + args.try_emplace("x-nv-video[0].dynamicRangeMode"sv, "0"sv); + args.try_emplace("x-nv-aqos.packetDuration"sv, "5"sv); + args.try_emplace("x-nv-general.useReliableUdp"sv, "1"sv); + args.try_emplace("x-nv-vqos[0].fec.minRequiredFecPackets"sv, "0"sv); + args.try_emplace("x-nv-general.featureFlags"sv, "135"sv); + args.try_emplace("x-nv-vqos[0].qosTrafficType"sv, "5"sv); + args.try_emplace("x-nv-aqos.qosTrafficType"sv, "4"sv); + + stream::config_t config; + + config.audio.flags[audio::config_t::HOST_AUDIO] = launch_session->host_audio; + try { + config.audio.channels = util::from_view(args.at("x-nv-audio.surround.numChannels"sv)); + config.audio.mask = util::from_view(args.at("x-nv-audio.surround.channelMask"sv)); + config.audio.packetDuration = util::from_view(args.at("x-nv-aqos.packetDuration"sv)); + + config.audio.flags[audio::config_t::HIGH_QUALITY] = + util::from_view(args.at("x-nv-audio.surround.AudioQuality"sv)); + + config.controlProtocolType = util::from_view(args.at("x-nv-general.useReliableUdp"sv)); + config.packetsize = util::from_view(args.at("x-nv-video[0].packetSize"sv)); + config.minRequiredFecPackets = util::from_view(args.at("x-nv-vqos[0].fec.minRequiredFecPackets"sv)); + config.featureFlags = util::from_view(args.at("x-nv-general.featureFlags"sv)); + config.audioQosType = util::from_view(args.at("x-nv-aqos.qosTrafficType"sv)); + config.videoQosType = util::from_view(args.at("x-nv-vqos[0].qosTrafficType"sv)); + + config.monitor.height = util::from_view(args.at("x-nv-video[0].clientViewportHt"sv)); + config.monitor.width = util::from_view(args.at("x-nv-video[0].clientViewportWd"sv)); + config.monitor.framerate = util::from_view(args.at("x-nv-video[0].maxFPS"sv)); + config.monitor.bitrate = util::from_view(args.at("x-nv-vqos[0].bw.maximumBitrateKbps"sv)); + config.monitor.slicesPerFrame = util::from_view(args.at("x-nv-video[0].videoEncoderSlicesPerFrame"sv)); + config.monitor.numRefFrames = util::from_view(args.at("x-nv-video[0].maxNumReferenceFrames"sv)); + config.monitor.encoderCscMode = util::from_view(args.at("x-nv-video[0].encoderCscMode"sv)); + config.monitor.videoFormat = util::from_view(args.at("x-nv-vqos[0].bitStreamFormat"sv)); + config.monitor.dynamicRange = util::from_view(args.at("x-nv-video[0].dynamicRangeMode"sv)); + } + catch (std::out_of_range &) { + respond(sock, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); + return; + } + + // When using stereo audio, the audio quality is (strangely) indicated by whether the Host field + // in the RTSP message matches a local interface's IP address. Fortunately, Moonlight always sends + // 0.0.0.0 when it wants low quality, so it is easy to check without enumerating interfaces. + if (config.audio.channels == 2) { + for (auto option = req->options; option != nullptr; option = option->next) { + if ("Host"sv == option->option) { + std::string_view content { option->content }; + BOOST_LOG(debug) << "Found Host: "sv << content; + config.audio.flags[audio::config_t::HIGH_QUALITY] = (content.find("0.0.0.0"sv) == std::string::npos); + } + } + } + + if (config.monitor.videoFormat != 0 && config::video.hevc_mode == 1) { + BOOST_LOG(warning) << "HEVC is disabled, yet the client requested HEVC"sv; + + respond(sock, &option, 400, "BAD REQUEST", req->sequenceNumber, {}); + return; + } + + auto session = stream::session::alloc(config, launch_session->gcm_key, launch_session->iv); + + auto slot = server->accept(session); + if (!slot) { + BOOST_LOG(info) << "Ran out of slots for client from ["sv << ']'; + + respond(sock, &option, 503, "Service Unavailable", req->sequenceNumber, {}); + return; + } + + if (stream::session::start(*session, sock.remote_endpoint().address().to_string())) { + BOOST_LOG(error) << "Failed to start a streaming session"sv; + + server->clear(slot); + respond(sock, &option, 500, "Internal Server Error", req->sequenceNumber, {}); + return; + } + + respond(sock, &option, 200, "OK", req->sequenceNumber, {}); } - for(auto option = msg->options; option != nullptr; option = option->next) { - std::string_view content { option->content }; - std::string_view name { option->option }; + void + cmd_play(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) { + OPTION_ITEM option {}; - BOOST_LOG(debug) << name << " :: "sv << content; + // I know these string literals will not be modified + option.option = const_cast("CSeq"); + + auto seqn_str = std::to_string(req->sequenceNumber); + option.content = const_cast(seqn_str.c_str()); + + respond(sock, &option, 200, "OK", req->sequenceNumber, {}); } - BOOST_LOG(debug) << "---Begin MessageBuffer---"sv << std::endl - << messageBuffer << std::endl - << "---End MessageBuffer---"sv << std::endl; -} -} // namespace rtsp_stream + void + rtpThread() { + auto shutdown_event = mail::man->event(mail::shutdown); + auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); + + server.map("OPTIONS"sv, &cmd_option); + server.map("DESCRIBE"sv, &cmd_describe); + server.map("SETUP"sv, &cmd_setup); + server.map("ANNOUNCE"sv, &cmd_announce); + + server.map("PLAY"sv, &cmd_play); + + boost::system::error_code ec; + if (server.bind(map_port(rtsp_stream::RTSP_SETUP_PORT), ec)) { + BOOST_LOG(fatal) << "Couldn't bind RTSP server to port ["sv << map_port(rtsp_stream::RTSP_SETUP_PORT) << "], " << ec.message(); + shutdown_event->raise(true); + + return; + } + + while (!shutdown_event->peek()) { + server.iterate(std::min(500ms, config::stream.ping_timeout)); + + if (broadcast_shutdown_event->peek()) { + server.clear(); + } + else { + // cleanup all stopped sessions + server.clear(false); + } + } + + server.clear(); + } + + void + print_msg(PRTSP_MESSAGE msg) { + std::string_view type = msg->type == TYPE_RESPONSE ? "RESPONSE"sv : "REQUEST"sv; + + std::string_view payload { msg->payload, (size_t) msg->payloadLength }; + std::string_view protocol { msg->protocol }; + auto seqnm = msg->sequenceNumber; + std::string_view messageBuffer { msg->messageBuffer }; + + BOOST_LOG(debug) << "type ["sv << type << ']'; + BOOST_LOG(debug) << "sequence number ["sv << seqnm << ']'; + BOOST_LOG(debug) << "protocol :: "sv << protocol; + BOOST_LOG(debug) << "payload :: "sv << payload; + + if (msg->type == TYPE_RESPONSE) { + auto &resp = msg->message.response; + + auto statuscode = resp.statusCode; + std::string_view status { resp.statusString }; + + BOOST_LOG(debug) << "statuscode :: "sv << statuscode; + BOOST_LOG(debug) << "status :: "sv << status; + } + else { + auto &req = msg->message.request; + + std::string_view command { req.command }; + std::string_view target { req.target }; + + BOOST_LOG(debug) << "command :: "sv << command; + BOOST_LOG(debug) << "target :: "sv << target; + } + + for (auto option = msg->options; option != nullptr; option = option->next) { + std::string_view content { option->content }; + std::string_view name { option->option }; + + BOOST_LOG(debug) << name << " :: "sv << content; + } + + BOOST_LOG(debug) << "---Begin MessageBuffer---"sv << std::endl + << messageBuffer << std::endl + << "---End MessageBuffer---"sv << std::endl; + } +} // namespace rtsp_stream diff --git a/src/rtsp.h b/src/rtsp.h index 666ce2b9..f33030a0 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -9,20 +9,23 @@ #include "thread_safe.h" namespace rtsp_stream { -constexpr auto RTSP_SETUP_PORT = 21; + constexpr auto RTSP_SETUP_PORT = 21; -struct launch_session_t { - crypto::aes_t gcm_key; - crypto::aes_t iv; + struct launch_session_t { + crypto::aes_t gcm_key; + crypto::aes_t iv; - bool host_audio; -}; + bool host_audio; + }; -void launch_session_raise(launch_session_t launch_session); -int session_count(); + void + launch_session_raise(launch_session_t launch_session); + int + session_count(); -void rtpThread(); + void + rtpThread(); -} // namespace rtsp_stream +} // namespace rtsp_stream -#endif // SUNSHINE_RTSP_H +#endif // SUNSHINE_RTSP_H diff --git a/src/stream.cpp b/src/stream.cpp index 3a9e761d..65490593 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -36,22 +36,22 @@ extern "C" { #define IDX_HDR_MODE 11 static const short packetTypes[] = { - 0x0305, // Start A - 0x0307, // Start B - 0x0301, // Invalidate reference frames - 0x0201, // Loss Stats - 0x0204, // Frame Stats (unused) - 0x0206, // Input data - 0x010b, // Rumble data - 0x0100, // Termination - 0x0200, // Periodic Ping - 0x0302, // IDR frame - 0x0001, // fully encrypted - 0x010e, // HDR mode + 0x0305, // Start A + 0x0307, // Start B + 0x0301, // Invalidate reference frames + 0x0201, // Loss Stats + 0x0204, // Frame Stats (unused) + 0x0206, // Input data + 0x010b, // Rumble data + 0x0100, // Termination + 0x0200, // Periodic Ping + 0x0302, // IDR frame + 0x0001, // fully encrypted + 0x010e, // HDR mode }; namespace asio = boost::asio; -namespace sys = boost::system; +namespace sys = boost::system; using asio::ip::tcp; using asio::ip::udp; @@ -60,1558 +60,1598 @@ using namespace std::literals; namespace stream { -enum class socket_e : int { - video, - audio -}; + enum class socket_e : int { + video, + audio + }; #pragma pack(push, 1) -struct video_short_frame_header_t { - uint8_t *payload() { - return (uint8_t *)(this + 1); - } + struct video_short_frame_header_t { + uint8_t * + payload() { + return (uint8_t *) (this + 1); + } - std::uint8_t headerType; // Always 0x01 for short headers - std::uint8_t unknown[2]; + std::uint8_t headerType; // Always 0x01 for short headers + std::uint8_t unknown[2]; - // Currently known values: - // 1 = Normal P-frame - // 2 = IDR-frame - // 4 = P-frame with intra-refresh blocks - // 5 = P-frame after reference frame invalidation - std::uint8_t frameType; + // Currently known values: + // 1 = Normal P-frame + // 2 = IDR-frame + // 4 = P-frame with intra-refresh blocks + // 5 = P-frame after reference frame invalidation + std::uint8_t frameType; - std::uint8_t unknown2[4]; -}; + std::uint8_t unknown2[4]; + }; -static_assert( - sizeof(video_short_frame_header_t) == 8, - "Short frame header must be 8 bytes"); + static_assert( + sizeof(video_short_frame_header_t) == 8, + "Short frame header must be 8 bytes"); -struct video_packet_raw_t { - uint8_t *payload() { - return (uint8_t *)(this + 1); - } + struct video_packet_raw_t { + uint8_t * + payload() { + return (uint8_t *) (this + 1); + } - RTP_PACKET rtp; - char reserved[4]; + RTP_PACKET rtp; + char reserved[4]; - NV_VIDEO_PACKET packet; -}; + NV_VIDEO_PACKET packet; + }; -struct audio_packet_raw_t { - uint8_t *payload() { - return (uint8_t *)(this + 1); - } + struct audio_packet_raw_t { + uint8_t * + payload() { + return (uint8_t *) (this + 1); + } - RTP_PACKET rtp; -}; + RTP_PACKET rtp; + }; -struct control_header_v2 { - std::uint16_t type; - std::uint16_t payloadLength; + struct control_header_v2 { + std::uint16_t type; + std::uint16_t payloadLength; - uint8_t *payload() { - return (uint8_t *)(this + 1); - } -}; + uint8_t * + payload() { + return (uint8_t *) (this + 1); + } + }; -struct control_terminate_t { - control_header_v2 header; + struct control_terminate_t { + control_header_v2 header; - std::uint32_t ec; -}; + std::uint32_t ec; + }; -struct control_rumble_t { - control_header_v2 header; + struct control_rumble_t { + control_header_v2 header; - std::uint32_t useless; + std::uint32_t useless; - std::uint16_t id; - std::uint16_t lowfreq; - std::uint16_t highfreq; -}; + std::uint16_t id; + std::uint16_t lowfreq; + std::uint16_t highfreq; + }; -struct control_hdr_mode_t { - control_header_v2 header; + struct control_hdr_mode_t { + control_header_v2 header; - std::uint8_t enabled; + std::uint8_t enabled; - // Sunshine protocol extension - SS_HDR_METADATA metadata; -}; + // Sunshine protocol extension + SS_HDR_METADATA metadata; + }; -typedef struct control_encrypted_t { - std::uint16_t encryptedHeaderType; // Always LE 0x0001 - std::uint16_t length; // sizeof(seq) + 16 byte tag + secondary header and data + typedef struct control_encrypted_t { + std::uint16_t encryptedHeaderType; // Always LE 0x0001 + std::uint16_t length; // sizeof(seq) + 16 byte tag + secondary header and data - // seq is accepted as an arbitrary value in Moonlight - std::uint32_t seq; // Monotonically increasing sequence number (used as IV for AES-GCM) + // seq is accepted as an arbitrary value in Moonlight + std::uint32_t seq; // Monotonically increasing sequence number (used as IV for AES-GCM) - uint8_t *payload() { - return (uint8_t *)(this + 1); - } - // encrypted control_header_v2 and payload data follow -} *control_encrypted_p; + uint8_t * + payload() { + return (uint8_t *) (this + 1); + } + // encrypted control_header_v2 and payload data follow + } *control_encrypted_p; -struct audio_fec_packet_raw_t { - uint8_t *payload() { - return (uint8_t *)(this + 1); - } + struct audio_fec_packet_raw_t { + uint8_t * + payload() { + return (uint8_t *) (this + 1); + } - RTP_PACKET rtp; - AUDIO_FEC_HEADER fecHeader; -}; + RTP_PACKET rtp; + AUDIO_FEC_HEADER fecHeader; + }; #pragma pack(pop) -constexpr std::size_t round_to_pkcs7_padded(std::size_t size) { - return ((size + 15) / 16) * 16; -} -constexpr std::size_t MAX_AUDIO_PACKET_SIZE = 1400; - -using rh_t = util::safe_ptr; -using video_packet_t = util::c_ptr; -using audio_packet_t = util::c_ptr; -using audio_fec_packet_t = util::c_ptr; -using audio_aes_t = std::array; - -using message_queue_t = std::shared_ptr>>; -using message_queue_queue_t = std::shared_ptr>>; - -// return bytes written on success -// return -1 on error -static inline int encode_audio(int featureSet, const audio::buffer_t &plaintext, audio_packet_t &destination, std::uint32_t avRiKeyIv, crypto::cipher::cbc_t &cbc) { - // If encryption isn't enabled - if(!(featureSet & 0x20)) { - std::copy(std::begin(plaintext), std::end(plaintext), destination->payload()); - return plaintext.size(); + constexpr std::size_t + round_to_pkcs7_padded(std::size_t size) { + return ((size + 15) / 16) * 16; } + constexpr std::size_t MAX_AUDIO_PACKET_SIZE = 1400; - crypto::aes_t iv {}; - *(std::uint32_t *)iv.data() = util::endian::big(avRiKeyIv + destination->rtp.sequenceNumber); + using rh_t = util::safe_ptr; + using video_packet_t = util::c_ptr; + using audio_packet_t = util::c_ptr; + using audio_fec_packet_t = util::c_ptr; + using audio_aes_t = std::array; - return cbc.encrypt(std::string_view { (char *)std::begin(plaintext), plaintext.size() }, destination->payload(), &iv); -} + using message_queue_t = std::shared_ptr>>; + using message_queue_queue_t = std::shared_ptr>>; -static inline void while_starting_do_nothing(std::atomic &state) { - while(state.load(std::memory_order_acquire) == session::state_e::STARTING) { - std::this_thread::sleep_for(1ms); - } -} - -class control_server_t { -public: - int bind(std::uint16_t port) { - _host = net::host_create(_addr, config::stream.channels, port); - - return !(bool)_host; - } - - void emplace_addr_to_session(const std::string &addr, session_t &session) { - auto lg = _map_addr_session.lock(); - - _map_addr_session->emplace(addr, std::make_pair(0u, &session)); - } - - // Get session associated with address. - // If none are found, try to find a session not yet claimed. (It will be marked by a port of value 0 - // If none of those are found, return nullptr - session_t *get_session(const net::peer_t peer); - - // Circular dependency: - // iterate refers to session - // session refers to broadcast_ctx_t - // broadcast_ctx_t refers to control_server_t - // Therefore, iterate is implemented further down the source file - void iterate(std::chrono::milliseconds timeout); - - void call(std::uint16_t type, session_t *session, const std::string_view &payload); - - void map(uint16_t type, std::function cb) { - _map_type_cb.emplace(type, std::move(cb)); - } - - int send(const std::string_view &payload, net::peer_t peer) { - auto packet = enet_packet_create(payload.data(), payload.size(), ENET_PACKET_FLAG_RELIABLE); - if(enet_peer_send(peer, 0, packet)) { - enet_packet_destroy(packet); - - return -1; + // return bytes written on success + // return -1 on error + static inline int + encode_audio(int featureSet, const audio::buffer_t &plaintext, audio_packet_t &destination, std::uint32_t avRiKeyIv, crypto::cipher::cbc_t &cbc) { + // If encryption isn't enabled + if (!(featureSet & 0x20)) { + std::copy(std::begin(plaintext), std::end(plaintext), destination->payload()); + return plaintext.size(); } - return 0; + crypto::aes_t iv {}; + *(std::uint32_t *) iv.data() = util::endian::big(avRiKeyIv + destination->rtp.sequenceNumber); + + return cbc.encrypt(std::string_view { (char *) std::begin(plaintext), plaintext.size() }, destination->payload(), &iv); } - void flush() { - enet_host_flush(_host.get()); + static inline void + while_starting_do_nothing(std::atomic &state) { + while (state.load(std::memory_order_acquire) == session::state_e::STARTING) { + std::this_thread::sleep_for(1ms); + } } - // Callbacks - std::unordered_map> _map_type_cb; + class control_server_t { + public: + int + bind(std::uint16_t port) { + _host = net::host_create(_addr, config::stream.channels, port); - // Mapping ip:port to session - sync_util::sync_t>> _map_addr_session; + return !(bool) _host; + } - ENetAddress _addr; - net::host_t _host; -}; + void + emplace_addr_to_session(const std::string &addr, session_t &session) { + auto lg = _map_addr_session.lock(); -struct broadcast_ctx_t { - message_queue_queue_t message_queue_queue; + _map_addr_session->emplace(addr, std::make_pair(0u, &session)); + } - std::thread recv_thread; - std::thread video_thread; - std::thread audio_thread; - std::thread control_thread; + // Get session associated with address. + // If none are found, try to find a session not yet claimed. (It will be marked by a port of value 0 + // If none of those are found, return nullptr + session_t * + get_session(const net::peer_t peer); - asio::io_service io; + // Circular dependency: + // iterate refers to session + // session refers to broadcast_ctx_t + // broadcast_ctx_t refers to control_server_t + // Therefore, iterate is implemented further down the source file + void + iterate(std::chrono::milliseconds timeout); - udp::socket video_sock { io }; - udp::socket audio_sock { io }; + void + call(std::uint16_t type, session_t *session, const std::string_view &payload); - // This is purely for administrative purposes. - // It's possible two instances of Moonlight are behind a NAT. - // From Sunshine's point of view, the ip addresses are identical - // We need some way to know what ports are already used for different streams - sync_util::sync_t>> audio_video_connections; + void + map(uint16_t type, std::function cb) { + _map_type_cb.emplace(type, std::move(cb)); + } - control_server_t control_server; -}; + int + send(const std::string_view &payload, net::peer_t peer) { + auto packet = enet_packet_create(payload.data(), payload.size(), ENET_PACKET_FLAG_RELIABLE); + if (enet_peer_send(peer, 0, packet)) { + enet_packet_destroy(packet); -struct session_t { - config_t config; + return -1; + } - safe::mail_t mail; + return 0; + } - std::shared_ptr input; + void + flush() { + enet_host_flush(_host.get()); + } - std::thread audioThread; - std::thread videoThread; + // Callbacks + std::unordered_map> _map_type_cb; - std::chrono::steady_clock::time_point pingTimeout; + // Mapping ip:port to session + sync_util::sync_t>> _map_addr_session; - safe::shared_t::ptr_t broadcast_ref; + ENetAddress _addr; + net::host_t _host; + }; - struct { - int lowseq; - udp::endpoint peer; - safe::mail_raw_t::event_t idr_events; - std::unique_ptr qos; - } video; + struct broadcast_ctx_t { + message_queue_queue_t message_queue_queue; - struct { - crypto::cipher::cbc_t cipher; + std::thread recv_thread; + std::thread video_thread; + std::thread audio_thread; + std::thread control_thread; - std::uint16_t sequenceNumber; - // avRiKeyId == util::endian::big(First (sizeof(avRiKeyId)) bytes of launch_session->iv) - std::uint32_t avRiKeyId; - std::uint32_t timestamp; - udp::endpoint peer; + asio::io_service io; - util::buffer_t shards; - util::buffer_t shards_p; + udp::socket video_sock { io }; + udp::socket audio_sock { io }; - audio_fec_packet_t fec_packet; - std::unique_ptr qos; - } audio; + // This is purely for administrative purposes. + // It's possible two instances of Moonlight are behind a NAT. + // From Sunshine's point of view, the ip addresses are identical + // We need some way to know what ports are already used for different streams + sync_util::sync_t>> audio_video_connections; - struct { - crypto::cipher::gcm_t cipher; - crypto::aes_t iv; + control_server_t control_server; + }; - net::peer_t peer; - std::uint8_t seq; + struct session_t { + config_t config; - platf::rumble_queue_t rumble_queue; - safe::mail_raw_t::event_t hdr_queue; - } control; + safe::mail_t mail; - safe::mail_raw_t::event_t shutdown_event; - safe::signal_t controlEnd; + std::shared_ptr input; - std::atomic state; -}; + std::thread audioThread; + std::thread videoThread; -/** + std::chrono::steady_clock::time_point pingTimeout; + + safe::shared_t::ptr_t broadcast_ref; + + struct { + int lowseq; + udp::endpoint peer; + safe::mail_raw_t::event_t idr_events; + std::unique_ptr qos; + } video; + + struct { + crypto::cipher::cbc_t cipher; + + std::uint16_t sequenceNumber; + // avRiKeyId == util::endian::big(First (sizeof(avRiKeyId)) bytes of launch_session->iv) + std::uint32_t avRiKeyId; + std::uint32_t timestamp; + udp::endpoint peer; + + util::buffer_t shards; + util::buffer_t shards_p; + + audio_fec_packet_t fec_packet; + std::unique_ptr qos; + } audio; + + struct { + crypto::cipher::gcm_t cipher; + crypto::aes_t iv; + + net::peer_t peer; + std::uint8_t seq; + + platf::rumble_queue_t rumble_queue; + safe::mail_raw_t::event_t hdr_queue; + } control; + + safe::mail_raw_t::event_t shutdown_event; + safe::signal_t controlEnd; + + std::atomic state; + }; + + /** * First part of cipher must be struct of type control_encrypted_t * * returns empty string_view on failure * returns string_view pointing to payload data */ -template -static inline std::string_view encode_control(session_t *session, const std::string_view &plaintext, std::array &tagged_cipher) { - static_assert( - max_payload_size >= sizeof(control_encrypted_t) + sizeof(crypto::cipher::tag_size), - "max_payload_size >= sizeof(control_encrypted_t) + sizeof(crypto::cipher::tag_size)"); + template + static inline std::string_view + encode_control(session_t *session, const std::string_view &plaintext, std::array &tagged_cipher) { + static_assert( + max_payload_size >= sizeof(control_encrypted_t) + sizeof(crypto::cipher::tag_size), + "max_payload_size >= sizeof(control_encrypted_t) + sizeof(crypto::cipher::tag_size)"); + if (session->config.controlProtocolType != 13) { + return plaintext; + } - if(session->config.controlProtocolType != 13) { - return plaintext; + crypto::aes_t iv {}; + auto seq = session->control.seq++; + iv[0] = seq; + + auto packet = (control_encrypted_p) tagged_cipher.data(); + + auto bytes = session->control.cipher.encrypt(plaintext, packet->payload(), &iv); + if (bytes <= 0) { + BOOST_LOG(error) << "Couldn't encrypt control data"sv; + return {}; + } + + std::uint16_t packet_length = bytes + crypto::cipher::tag_size + sizeof(control_encrypted_t::seq); + + packet->encryptedHeaderType = util::endian::little(0x0001); + packet->length = util::endian::little(packet_length); + packet->seq = util::endian::little(seq); + + return std::string_view { (char *) tagged_cipher.data(), packet_length + sizeof(control_encrypted_t) - sizeof(control_encrypted_t::seq) }; } - crypto::aes_t iv {}; - auto seq = session->control.seq++; - iv[0] = seq; + int + start_broadcast(broadcast_ctx_t &ctx); + void + end_broadcast(broadcast_ctx_t &ctx); - auto packet = (control_encrypted_p)tagged_cipher.data(); + static auto broadcast = safe::make_shared(start_broadcast, end_broadcast); - auto bytes = session->control.cipher.encrypt(plaintext, packet->payload(), &iv); - if(bytes <= 0) { - BOOST_LOG(error) << "Couldn't encrypt control data"sv; - return {}; - } + session_t * + control_server_t::get_session(const net::peer_t peer) { + TUPLE_2D(port, addr_string, platf::from_sockaddr_ex((sockaddr *) &peer->address.address)); - std::uint16_t packet_length = bytes + crypto::cipher::tag_size + sizeof(control_encrypted_t::seq); + auto lg = _map_addr_session.lock(); + TUPLE_2D(begin, end, _map_addr_session->equal_range(addr_string)); - packet->encryptedHeaderType = util::endian::little(0x0001); - packet->length = util::endian::little(packet_length); - packet->seq = util::endian::little(seq); + auto it = std::end(_map_addr_session.raw); + for (auto pos = begin; pos != end; ++pos) { + TUPLE_2D_REF(session_port, session_p, pos->second); - return std::string_view { (char *)tagged_cipher.data(), packet_length + sizeof(control_encrypted_t) - sizeof(control_encrypted_t::seq) }; -} + if (port == session_port) { + return session_p; + } + else if (session_port == 0) { + it = pos; + } + } -int start_broadcast(broadcast_ctx_t &ctx); -void end_broadcast(broadcast_ctx_t &ctx); + if (it != std::end(_map_addr_session.raw)) { + TUPLE_2D_REF(session_port, session_p, it->second); + session_p->control.peer = peer; + session_port = port; -static auto broadcast = safe::make_shared(start_broadcast, end_broadcast); - -session_t *control_server_t::get_session(const net::peer_t peer) { - TUPLE_2D(port, addr_string, platf::from_sockaddr_ex((sockaddr *)&peer->address.address)); - - auto lg = _map_addr_session.lock(); - TUPLE_2D(begin, end, _map_addr_session->equal_range(addr_string)); - - auto it = std::end(_map_addr_session.raw); - for(auto pos = begin; pos != end; ++pos) { - TUPLE_2D_REF(session_port, session_p, pos->second); - - if(port == session_port) { return session_p; } - else if(session_port == 0) { - it = pos; + + return nullptr; + } + + void + control_server_t::call(std::uint16_t type, session_t *session, const std::string_view &payload) { + auto cb = _map_type_cb.find(type); + if (cb == std::end(_map_type_cb)) { + BOOST_LOG(debug) + << "type [Unknown] { "sv << util::hex(type).to_string_view() << " }"sv << std::endl + << "---data---"sv << std::endl + << util::hex_vec(payload) << std::endl + << "---end data---"sv; + } + else { + cb->second(session, payload); } } - if(it != std::end(_map_addr_session.raw)) { - TUPLE_2D_REF(session_port, session_p, it->second); + void + control_server_t::iterate(std::chrono::milliseconds timeout) { + ENetEvent event; + auto res = enet_host_service(_host.get(), &event, timeout.count()); - session_p->control.peer = peer; - session_port = port; + if (res > 0) { + auto session = get_session(event.peer); + if (!session) { + BOOST_LOG(warning) << "Rejected connection from ["sv << platf::from_sockaddr((sockaddr *) &event.peer->address.address) << "]: it's not properly set up"sv; + enet_peer_disconnect_now(event.peer, 0); - return session_p; - } - - return nullptr; -} - -void control_server_t::call(std::uint16_t type, session_t *session, const std::string_view &payload) { - auto cb = _map_type_cb.find(type); - if(cb == std::end(_map_type_cb)) { - BOOST_LOG(debug) - << "type [Unknown] { "sv << util::hex(type).to_string_view() << " }"sv << std::endl - << "---data---"sv << std::endl - << util::hex_vec(payload) << std::endl - << "---end data---"sv; - } - else { - cb->second(session, payload); - } -} - -void control_server_t::iterate(std::chrono::milliseconds timeout) { - ENetEvent event; - auto res = enet_host_service(_host.get(), &event, timeout.count()); - - if(res > 0) { - auto session = get_session(event.peer); - if(!session) { - BOOST_LOG(warning) << "Rejected connection from ["sv << platf::from_sockaddr((sockaddr *)&event.peer->address.address) << "]: it's not properly set up"sv; - enet_peer_disconnect_now(event.peer, 0); - - return; - } - - session->pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; - - switch(event.type) { - case ENET_EVENT_TYPE_RECEIVE: { - net::packet_t packet { event.packet }; - - auto type = *(std::uint16_t *)packet->data; - std::string_view payload { (char *)packet->data + sizeof(type), packet->dataLength - sizeof(type) }; - - call(type, session, payload); - } break; - case ENET_EVENT_TYPE_CONNECT: - BOOST_LOG(info) << "CLIENT CONNECTED"sv; - break; - case ENET_EVENT_TYPE_DISCONNECT: - BOOST_LOG(info) << "CLIENT DISCONNECTED"sv; - // No more clients to send video data to ^_^ - if(session->state == session::state_e::RUNNING) { - session::stop(*session); + return; + } + + session->pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; + + switch (event.type) { + case ENET_EVENT_TYPE_RECEIVE: { + net::packet_t packet { event.packet }; + + auto type = *(std::uint16_t *) packet->data; + std::string_view payload { (char *) packet->data + sizeof(type), packet->dataLength - sizeof(type) }; + + call(type, session, payload); + } break; + case ENET_EVENT_TYPE_CONNECT: + BOOST_LOG(info) << "CLIENT CONNECTED"sv; + break; + case ENET_EVENT_TYPE_DISCONNECT: + BOOST_LOG(info) << "CLIENT DISCONNECTED"sv; + // No more clients to send video data to ^_^ + if (session->state == session::state_e::RUNNING) { + session::stop(*session); + } + break; + case ENET_EVENT_TYPE_NONE: + break; } - break; - case ENET_EVENT_TYPE_NONE: - break; } } -} -namespace fec { -using rs_t = util::safe_ptr; + namespace fec { + using rs_t = util::safe_ptr; -struct fec_t { - size_t data_shards; - size_t nr_shards; - size_t percentage; + struct fec_t { + size_t data_shards; + size_t nr_shards; + size_t percentage; - size_t blocksize; - util::buffer_t shards; + size_t blocksize; + util::buffer_t shards; - char *data(size_t el) { - return &shards[el * blocksize]; - } + char * + data(size_t el) { + return &shards[el * blocksize]; + } - std::string_view operator[](size_t el) const { - return { &shards[el * blocksize], blocksize }; - } + std::string_view + operator[](size_t el) const { + return { &shards[el * blocksize], blocksize }; + } - size_t size() const { - return nr_shards; - } -}; + size_t + size() const { + return nr_shards; + } + }; -static fec_t encode(const std::string_view &payload, size_t blocksize, size_t fecpercentage, size_t minparityshards) { - auto payload_size = payload.size(); + static fec_t + encode(const std::string_view &payload, size_t blocksize, size_t fecpercentage, size_t minparityshards) { + auto payload_size = payload.size(); - auto pad = payload_size % blocksize != 0; + auto pad = payload_size % blocksize != 0; - auto data_shards = payload_size / blocksize + (pad ? 1 : 0); - auto parity_shards = (data_shards * fecpercentage + 99) / 100; + auto data_shards = payload_size / blocksize + (pad ? 1 : 0); + auto parity_shards = (data_shards * fecpercentage + 99) / 100; - // increase the FEC percentage for this frame if the parity shard minimum is not met - if(parity_shards < minparityshards) { - parity_shards = minparityshards; - fecpercentage = (100 * parity_shards) / data_shards; + // increase the FEC percentage for this frame if the parity shard minimum is not met + if (parity_shards < minparityshards) { + parity_shards = minparityshards; + fecpercentage = (100 * parity_shards) / data_shards; - BOOST_LOG(verbose) << "Increasing FEC percentage to "sv << fecpercentage << " to meet parity shard minimum"sv << std::endl; - } + BOOST_LOG(verbose) << "Increasing FEC percentage to "sv << fecpercentage << " to meet parity shard minimum"sv << std::endl; + } - auto nr_shards = data_shards + parity_shards; - if(nr_shards > DATA_SHARDS_MAX) { - BOOST_LOG(warning) - << "Number of fragments for reed solomon exceeds DATA_SHARDS_MAX"sv << std::endl - << nr_shards << " > "sv << DATA_SHARDS_MAX - << ", skipping error correction"sv; + auto nr_shards = data_shards + parity_shards; + if (nr_shards > DATA_SHARDS_MAX) { + BOOST_LOG(warning) + << "Number of fragments for reed solomon exceeds DATA_SHARDS_MAX"sv << std::endl + << nr_shards << " > "sv << DATA_SHARDS_MAX + << ", skipping error correction"sv; - nr_shards = data_shards; - fecpercentage = 0; - } + nr_shards = data_shards; + fecpercentage = 0; + } - util::buffer_t shards { nr_shards * blocksize }; - util::buffer_t shards_p { nr_shards }; + util::buffer_t shards { nr_shards * blocksize }; + util::buffer_t shards_p { nr_shards }; - // copy payload + padding - auto next = std::copy(std::begin(payload), std::end(payload), std::begin(shards)); - std::fill(next, std::end(shards), 0); // padding with zero + // copy payload + padding + auto next = std::copy(std::begin(payload), std::end(payload), std::begin(shards)); + std::fill(next, std::end(shards), 0); // padding with zero - for(auto x = 0; x < nr_shards; ++x) { - shards_p[x] = (uint8_t *)&shards[x * blocksize]; - } + for (auto x = 0; x < nr_shards; ++x) { + shards_p[x] = (uint8_t *) &shards[x * blocksize]; + } - if(data_shards + parity_shards <= DATA_SHARDS_MAX) { - // packets = parity_shards + data_shards - rs_t rs { reed_solomon_new(data_shards, parity_shards) }; + if (data_shards + parity_shards <= DATA_SHARDS_MAX) { + // packets = parity_shards + data_shards + rs_t rs { reed_solomon_new(data_shards, parity_shards) }; - reed_solomon_encode(rs.get(), shards_p.begin(), nr_shards, blocksize); - } + reed_solomon_encode(rs.get(), shards_p.begin(), nr_shards, blocksize); + } - return { - data_shards, - nr_shards, - fecpercentage, - blocksize, - std::move(shards) - }; -} -} // namespace fec + return { + data_shards, + nr_shards, + fecpercentage, + blocksize, + std::move(shards) + }; + } + } // namespace fec -template -std::vector insert(uint64_t insert_size, uint64_t slice_size, const std::string_view &data, F &&f) { - auto pad = data.size() % slice_size != 0; - auto elements = data.size() / slice_size + (pad ? 1 : 0); + template + std::vector + insert(uint64_t insert_size, uint64_t slice_size, const std::string_view &data, F &&f) { + auto pad = data.size() % slice_size != 0; + auto elements = data.size() / slice_size + (pad ? 1 : 0); - std::vector result; - result.resize(elements * insert_size + data.size()); + std::vector result; + result.resize(elements * insert_size + data.size()); - auto next = std::begin(data); - for(auto x = 0; x < elements - 1; ++x) { + auto next = std::begin(data); + for (auto x = 0; x < elements - 1; ++x) { + void *p = &result[x * (insert_size + slice_size)]; + + f(p, x, elements); + + std::copy(next, next + slice_size, (char *) p + insert_size); + next += slice_size; + } + + auto x = elements - 1; void *p = &result[x * (insert_size + slice_size)]; f(p, x, elements); - std::copy(next, next + slice_size, (char *)p + insert_size); - next += slice_size; + std::copy(next, std::end(data), (char *) p + insert_size); + + return result; } - auto x = elements - 1; - void *p = &result[x * (insert_size + slice_size)]; + std::vector + replace(const std::string_view &original, const std::string_view &old, const std::string_view &_new) { + std::vector replaced; - f(p, x, elements); + auto begin = std::begin(original); + auto end = std::end(original); + auto next = std::search(begin, end, std::begin(old), std::end(old)); - std::copy(next, std::end(data), (char *)p + insert_size); + std::copy(begin, next, std::back_inserter(replaced)); + if (next != end) { + std::copy(std::begin(_new), std::end(_new), std::back_inserter(replaced)); + std::copy(next + old.size(), end, std::back_inserter(replaced)); + } - return result; -} - -std::vector replace(const std::string_view &original, const std::string_view &old, const std::string_view &_new) { - std::vector replaced; - - auto begin = std::begin(original); - auto end = std::end(original); - auto next = std::search(begin, end, std::begin(old), std::end(old)); - - std::copy(begin, next, std::back_inserter(replaced)); - if(next != end) { - std::copy(std::begin(_new), std::end(_new), std::back_inserter(replaced)); - std::copy(next + old.size(), end, std::back_inserter(replaced)); + return replaced; } - return replaced; -} - -int send_rumble(session_t *session, std::uint16_t id, std::uint16_t lowfreq, std::uint16_t highfreq) { - if(!session->control.peer) { - BOOST_LOG(warning) << "Couldn't send rumble data, still waiting for PING from Moonlight"sv; - // Still waiting for PING from Moonlight - return -1; - } - - control_rumble_t plaintext; - plaintext.header.type = packetTypes[IDX_RUMBLE_DATA]; - plaintext.header.payloadLength = sizeof(control_rumble_t) - sizeof(control_header_v2); - - plaintext.useless = 0xC0FFEE; - plaintext.id = util::endian::little(id); - plaintext.lowfreq = util::endian::little(lowfreq); - plaintext.highfreq = util::endian::little(highfreq); - - BOOST_LOG(verbose) << id << " :: "sv << util::hex(lowfreq).to_string_view() << " :: "sv << util::hex(highfreq).to_string_view(); - std::array - encrypted_payload; - - auto payload = encode_control(session, util::view(plaintext), encrypted_payload); - if(session->broadcast_ref->control_server.send(payload, session->control.peer)) { - TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *)&session->control.peer->address.address)); - BOOST_LOG(warning) << "Couldn't send termination code to ["sv << addr << ':' << port << ']'; - - return -1; - } - - BOOST_LOG(debug) << "Send gamepadnr ["sv << id << "] with lowfreq ["sv << lowfreq << "] and highfreq ["sv << highfreq << ']'; - - return 0; -} - -int send_hdr_mode(session_t *session, video::hdr_info_t hdr_info) { - if(!session->control.peer) { - BOOST_LOG(warning) << "Couldn't send HDR mode, still waiting for PING from Moonlight"sv; - // Still waiting for PING from Moonlight - return -1; - } - - control_hdr_mode_t plaintext {}; - plaintext.header.type = packetTypes[IDX_HDR_MODE]; - plaintext.header.payloadLength = sizeof(control_hdr_mode_t) - sizeof(control_header_v2); - - plaintext.enabled = hdr_info->enabled; - plaintext.metadata = hdr_info->metadata; - - std::array - encrypted_payload; - - auto payload = encode_control(session, util::view(plaintext), encrypted_payload); - if(session->broadcast_ref->control_server.send(payload, session->control.peer)) { - TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *)&session->control.peer->address.address)); - BOOST_LOG(warning) << "Couldn't send HDR mode to ["sv << addr << ':' << port << ']'; - - return -1; - } - - BOOST_LOG(debug) << "Sent HDR mode: " << hdr_info->enabled; - return 0; -} - -void controlBroadcastThread(control_server_t *server) { - server->map(packetTypes[IDX_PERIODIC_PING], [](session_t *session, const std::string_view &payload) { - BOOST_LOG(verbose) << "type [IDX_START_A]"sv; - }); - - server->map(packetTypes[IDX_START_A], [&](session_t *session, const std::string_view &payload) { - BOOST_LOG(debug) << "type [IDX_START_A]"sv; - }); - - server->map(packetTypes[IDX_START_B], [&](session_t *session, const std::string_view &payload) { - BOOST_LOG(debug) << "type [IDX_START_B]"sv; - }); - - server->map(packetTypes[IDX_LOSS_STATS], [&](session_t *session, const std::string_view &payload) { - int32_t *stats = (int32_t *)payload.data(); - auto count = stats[0]; - std::chrono::milliseconds t { stats[1] }; - - auto lastGoodFrame = stats[3]; - - BOOST_LOG(verbose) - << "type [IDX_LOSS_STATS]"sv << std::endl - << "---begin stats---" << std::endl - << "loss count since last report [" << count << ']' << std::endl - << "time in milli since last report [" << t.count() << ']' << std::endl - << "last good frame [" << lastGoodFrame << ']' << std::endl - << "---end stats---"; - }); - - server->map(packetTypes[IDX_REQUEST_IDR_FRAME], [&](session_t *session, const std::string_view &payload) { - BOOST_LOG(debug) << "type [IDX_REQUEST_IDR_FRAME]"sv; - - session->video.idr_events->raise(true); - }); - - server->map(packetTypes[IDX_INVALIDATE_REF_FRAMES], [&](session_t *session, const std::string_view &payload) { - auto frames = (std::int64_t *)payload.data(); - auto firstFrame = frames[0]; - auto lastFrame = frames[1]; - - BOOST_LOG(debug) - << "type [IDX_INVALIDATE_REF_FRAMES]"sv << std::endl - << "firstFrame [" << firstFrame << ']' << std::endl - << "lastFrame [" << lastFrame << ']'; - - session->video.idr_events->raise(true); - }); - - server->map(packetTypes[IDX_INPUT_DATA], [&](session_t *session, const std::string_view &payload) { - BOOST_LOG(debug) << "type [IDX_INPUT_DATA]"sv; - - auto tagged_cipher_length = util::endian::big(*(int32_t *)payload.data()); - std::string_view tagged_cipher { payload.data() + sizeof(tagged_cipher_length), (size_t)tagged_cipher_length }; - - std::vector plaintext; - - auto &cipher = session->control.cipher; - auto &iv = session->control.iv; - if(cipher.decrypt(tagged_cipher, plaintext, &iv)) { - // something went wrong :( - - BOOST_LOG(error) << "Failed to verify tag"sv; - - session::stop(*session); - return; + int + send_rumble(session_t *session, std::uint16_t id, std::uint16_t lowfreq, std::uint16_t highfreq) { + if (!session->control.peer) { + BOOST_LOG(warning) << "Couldn't send rumble data, still waiting for PING from Moonlight"sv; + // Still waiting for PING from Moonlight + return -1; } - if(tagged_cipher_length >= 16 + sizeof(crypto::aes_t)) { - std::copy(payload.end() - 16, payload.end(), std::begin(iv)); - } + control_rumble_t plaintext; + plaintext.header.type = packetTypes[IDX_RUMBLE_DATA]; + plaintext.header.payloadLength = sizeof(control_rumble_t) - sizeof(control_header_v2); - input::print(plaintext.data()); - input::passthrough(session->input, std::move(plaintext)); - }); + plaintext.useless = 0xC0FFEE; + plaintext.id = util::endian::little(id); + plaintext.lowfreq = util::endian::little(lowfreq); + plaintext.highfreq = util::endian::little(highfreq); - server->map(packetTypes[IDX_ENCRYPTED], [server](session_t *session, const std::string_view &payload) { - BOOST_LOG(verbose) << "type [IDX_ENCRYPTED]"sv; - - auto header = (control_encrypted_p)(payload.data() - 2); - - auto length = util::endian::little(header->length); - auto seq = util::endian::little(header->seq); - - if(length < (16 + 4 + 4)) { - BOOST_LOG(warning) << "Control: Runt packet"sv; - return; - } - - auto tagged_cipher_length = length - 4; - std::string_view tagged_cipher { (char *)header->payload(), (size_t)tagged_cipher_length }; - - auto &cipher = session->control.cipher; - crypto::aes_t iv {}; - iv[0] = (std::uint8_t)seq; - - // update control sequence - ++session->control.seq; - - std::vector plaintext; - if(cipher.decrypt(tagged_cipher, plaintext, &iv)) { - // something went wrong :( - - BOOST_LOG(error) << "Failed to verify tag"sv; - - session::stop(*session); - return; - } - - // Ensure compatibility with old packet type - std::string_view next_payload { (char *)plaintext.data(), plaintext.size() }; - auto type = *(std::uint16_t *)next_payload.data(); - - if(type == packetTypes[IDX_ENCRYPTED]) { - BOOST_LOG(error) << "Bad packet type [IDX_ENCRYPTED] found"sv; - - session::stop(*session); - return; - } - - // IDX_INPUT_DATA will attempt to decrypt unencrypted data, therefore we need to skip it. - if(type != packetTypes[IDX_INPUT_DATA]) { - server->call(type, session, next_payload); - - return; - } - - // Ensure compatibility with IDX_INPUT_DATA - constexpr auto skip = sizeof(std::uint16_t) * 2; - plaintext.erase(std::begin(plaintext), std::begin(plaintext) + skip); - - input::print(plaintext.data()); - input::passthrough(session->input, std::move(plaintext)); - }); - - // This thread handles latency-sensitive control messages - platf::adjust_thread_priority(platf::thread_priority_e::critical); - - auto shutdown_event = mail::man->event(mail::broadcast_shutdown); - while(!shutdown_event->peek()) { - { - auto lg = server->_map_addr_session.lock(); - - auto now = std::chrono::steady_clock::now(); - - KITTY_WHILE_LOOP(auto pos = std::begin(*server->_map_addr_session), pos != std::end(*server->_map_addr_session), { - TUPLE_2D_REF(addr, port_session, *pos); - auto session = port_session.second; - - if(now > session->pingTimeout) { - BOOST_LOG(info) << addr << ": Ping Timeout"sv; - session::stop(*session); - } - - if(session->state.load(std::memory_order_acquire) == session::state_e::STOPPING) { - pos = server->_map_addr_session->erase(pos); - - if(session->control.peer) { - enet_peer_disconnect_now(session->control.peer, 0); - } - - session->controlEnd.raise(true); - continue; - } - - auto &rumble_queue = session->control.rumble_queue; - while(rumble_queue->peek()) { - auto rumble = rumble_queue->pop(); - - send_rumble(session, rumble->id, rumble->lowfreq, rumble->highfreq); - } - - // Unlike rumble which we send as best-effort, HDR state messages are critical - // for proper functioning of some clients. We must wait to pop entries from - // the queue until we're sure we have a peer to send them to. - auto &hdr_queue = session->control.hdr_queue; - while(session->control.peer && hdr_queue->peek()) { - auto hdr_info = hdr_queue->pop(); - - send_hdr_mode(session, std::move(hdr_info)); - } - - ++pos; - }) - } - - if(proc::proc.running() == 0) { - BOOST_LOG(debug) << "Process terminated"sv; - - break; - } - - server->iterate(150ms); - } - - // Let all remaining connections know the server is shutting down - // reason: graceful termination - std::uint32_t reason = 0x80030023; - - control_terminate_t plaintext; - plaintext.header.type = packetTypes[IDX_TERMINATION]; - plaintext.header.payloadLength = sizeof(plaintext.ec); - plaintext.ec = reason; - - std::array - encrypted_payload; - - auto lg = server->_map_addr_session.lock(); - for(auto pos = std::begin(*server->_map_addr_session); pos != std::end(*server->_map_addr_session); ++pos) { - auto session = pos->second.second; + BOOST_LOG(verbose) << id << " :: "sv << util::hex(lowfreq).to_string_view() << " :: "sv << util::hex(highfreq).to_string_view(); + std::array + encrypted_payload; auto payload = encode_control(session, util::view(plaintext), encrypted_payload); - - if(server->send(payload, session->control.peer)) { - TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *)&session->control.peer->address.address)); + if (session->broadcast_ref->control_server.send(payload, session->control.peer)) { + TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session->control.peer->address.address)); BOOST_LOG(warning) << "Couldn't send termination code to ["sv << addr << ':' << port << ']'; + + return -1; } - session->shutdown_event->raise(true); - session->controlEnd.raise(true); + BOOST_LOG(debug) << "Send gamepadnr ["sv << id << "] with lowfreq ["sv << lowfreq << "] and highfreq ["sv << highfreq << ']'; + + return 0; } - server->flush(); -} - -void recvThread(broadcast_ctx_t &ctx) { - std::map peer_to_video_session; - std::map peer_to_audio_session; - - auto &video_sock = ctx.video_sock; - auto &audio_sock = ctx.audio_sock; - - auto &message_queue_queue = ctx.message_queue_queue; - auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); - - auto &io = ctx.io; - - udp::endpoint peer; - - std::array buf[2]; - std::function recv_func[2]; - - auto populate_peer_to_session = [&]() { - while(message_queue_queue->peek()) { - auto message_queue_opt = message_queue_queue->pop(); - TUPLE_3D_REF(socket_type, addr, message_queue, *message_queue_opt); - - switch(socket_type) { - case socket_e::video: - if(message_queue) { - peer_to_video_session.emplace(addr, message_queue); - } - else { - peer_to_video_session.erase(addr); - } - break; - case socket_e::audio: - if(message_queue) { - peer_to_audio_session.emplace(addr, message_queue); - } - else { - peer_to_audio_session.erase(addr); - } - break; - } + int + send_hdr_mode(session_t *session, video::hdr_info_t hdr_info) { + if (!session->control.peer) { + BOOST_LOG(warning) << "Couldn't send HDR mode, still waiting for PING from Moonlight"sv; + // Still waiting for PING from Moonlight + return -1; } - }; - auto recv_func_init = [&](udp::socket &sock, int buf_elem, std::map &peer_to_session) { - recv_func[buf_elem] = [&, buf_elem](const boost::system::error_code &ec, size_t bytes) { - auto fg = util::fail_guard([&]() { - sock.async_receive_from(asio::buffer(buf[buf_elem]), peer, 0, recv_func[buf_elem]); - }); + control_hdr_mode_t plaintext {}; + plaintext.header.type = packetTypes[IDX_HDR_MODE]; + plaintext.header.payloadLength = sizeof(control_hdr_mode_t) - sizeof(control_header_v2); - auto type_str = buf_elem ? "AUDIO"sv : "VIDEO"sv; - BOOST_LOG(verbose) << "Recv: "sv << peer.address().to_string() << ':' << peer.port() << " :: " << type_str; + plaintext.enabled = hdr_info->enabled; + plaintext.metadata = hdr_info->metadata; + std::array + encrypted_payload; - populate_peer_to_session(); + auto payload = encode_control(session, util::view(plaintext), encrypted_payload); + if (session->broadcast_ref->control_server.send(payload, session->control.peer)) { + TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session->control.peer->address.address)); + BOOST_LOG(warning) << "Couldn't send HDR mode to ["sv << addr << ':' << port << ']'; - // No data, yet no error - if(ec == boost::system::errc::connection_refused || ec == boost::system::errc::connection_reset) { + return -1; + } + + BOOST_LOG(debug) << "Sent HDR mode: " << hdr_info->enabled; + return 0; + } + + void + controlBroadcastThread(control_server_t *server) { + server->map(packetTypes[IDX_PERIODIC_PING], [](session_t *session, const std::string_view &payload) { + BOOST_LOG(verbose) << "type [IDX_START_A]"sv; + }); + + server->map(packetTypes[IDX_START_A], [&](session_t *session, const std::string_view &payload) { + BOOST_LOG(debug) << "type [IDX_START_A]"sv; + }); + + server->map(packetTypes[IDX_START_B], [&](session_t *session, const std::string_view &payload) { + BOOST_LOG(debug) << "type [IDX_START_B]"sv; + }); + + server->map(packetTypes[IDX_LOSS_STATS], [&](session_t *session, const std::string_view &payload) { + int32_t *stats = (int32_t *) payload.data(); + auto count = stats[0]; + std::chrono::milliseconds t { stats[1] }; + + auto lastGoodFrame = stats[3]; + + BOOST_LOG(verbose) + << "type [IDX_LOSS_STATS]"sv << std::endl + << "---begin stats---" << std::endl + << "loss count since last report [" << count << ']' << std::endl + << "time in milli since last report [" << t.count() << ']' << std::endl + << "last good frame [" << lastGoodFrame << ']' << std::endl + << "---end stats---"; + }); + + server->map(packetTypes[IDX_REQUEST_IDR_FRAME], [&](session_t *session, const std::string_view &payload) { + BOOST_LOG(debug) << "type [IDX_REQUEST_IDR_FRAME]"sv; + + session->video.idr_events->raise(true); + }); + + server->map(packetTypes[IDX_INVALIDATE_REF_FRAMES], [&](session_t *session, const std::string_view &payload) { + auto frames = (std::int64_t *) payload.data(); + auto firstFrame = frames[0]; + auto lastFrame = frames[1]; + + BOOST_LOG(debug) + << "type [IDX_INVALIDATE_REF_FRAMES]"sv << std::endl + << "firstFrame [" << firstFrame << ']' << std::endl + << "lastFrame [" << lastFrame << ']'; + + session->video.idr_events->raise(true); + }); + + server->map(packetTypes[IDX_INPUT_DATA], [&](session_t *session, const std::string_view &payload) { + BOOST_LOG(debug) << "type [IDX_INPUT_DATA]"sv; + + auto tagged_cipher_length = util::endian::big(*(int32_t *) payload.data()); + std::string_view tagged_cipher { payload.data() + sizeof(tagged_cipher_length), (size_t) tagged_cipher_length }; + + std::vector plaintext; + + auto &cipher = session->control.cipher; + auto &iv = session->control.iv; + if (cipher.decrypt(tagged_cipher, plaintext, &iv)) { + // something went wrong :( + + BOOST_LOG(error) << "Failed to verify tag"sv; + + session::stop(*session); return; } - if(ec || !bytes) { - BOOST_LOG(error) << "Couldn't receive data from udp socket: "sv << ec.message(); + if (tagged_cipher_length >= 16 + sizeof(crypto::aes_t)) { + std::copy(payload.end() - 16, payload.end(), std::begin(iv)); + } + + input::print(plaintext.data()); + input::passthrough(session->input, std::move(plaintext)); + }); + + server->map(packetTypes[IDX_ENCRYPTED], [server](session_t *session, const std::string_view &payload) { + BOOST_LOG(verbose) << "type [IDX_ENCRYPTED]"sv; + + auto header = (control_encrypted_p) (payload.data() - 2); + + auto length = util::endian::little(header->length); + auto seq = util::endian::little(header->seq); + + if (length < (16 + 4 + 4)) { + BOOST_LOG(warning) << "Control: Runt packet"sv; return; } - auto it = peer_to_session.find(peer.address()); - if(it != std::end(peer_to_session)) { - BOOST_LOG(debug) << "RAISE: "sv << peer.address().to_string() << ':' << peer.port() << " :: " << type_str; - it->second->raise(peer.port(), std::string { buf[buf_elem].data(), bytes }); + auto tagged_cipher_length = length - 4; + std::string_view tagged_cipher { (char *) header->payload(), (size_t) tagged_cipher_length }; + + auto &cipher = session->control.cipher; + crypto::aes_t iv {}; + iv[0] = (std::uint8_t) seq; + + // update control sequence + ++session->control.seq; + + std::vector plaintext; + if (cipher.decrypt(tagged_cipher, plaintext, &iv)) { + // something went wrong :( + + BOOST_LOG(error) << "Failed to verify tag"sv; + + session::stop(*session); + return; + } + + // Ensure compatibility with old packet type + std::string_view next_payload { (char *) plaintext.data(), plaintext.size() }; + auto type = *(std::uint16_t *) next_payload.data(); + + if (type == packetTypes[IDX_ENCRYPTED]) { + BOOST_LOG(error) << "Bad packet type [IDX_ENCRYPTED] found"sv; + + session::stop(*session); + return; + } + + // IDX_INPUT_DATA will attempt to decrypt unencrypted data, therefore we need to skip it. + if (type != packetTypes[IDX_INPUT_DATA]) { + server->call(type, session, next_payload); + + return; + } + + // Ensure compatibility with IDX_INPUT_DATA + constexpr auto skip = sizeof(std::uint16_t) * 2; + plaintext.erase(std::begin(plaintext), std::begin(plaintext) + skip); + + input::print(plaintext.data()); + input::passthrough(session->input, std::move(plaintext)); + }); + + // This thread handles latency-sensitive control messages + platf::adjust_thread_priority(platf::thread_priority_e::critical); + + auto shutdown_event = mail::man->event(mail::broadcast_shutdown); + while (!shutdown_event->peek()) { + { + auto lg = server->_map_addr_session.lock(); + + auto now = std::chrono::steady_clock::now(); + + KITTY_WHILE_LOOP(auto pos = std::begin(*server->_map_addr_session), pos != std::end(*server->_map_addr_session), { + TUPLE_2D_REF(addr, port_session, *pos); + auto session = port_session.second; + + if (now > session->pingTimeout) { + BOOST_LOG(info) << addr << ": Ping Timeout"sv; + session::stop(*session); + } + + if (session->state.load(std::memory_order_acquire) == session::state_e::STOPPING) { + pos = server->_map_addr_session->erase(pos); + + if (session->control.peer) { + enet_peer_disconnect_now(session->control.peer, 0); + } + + session->controlEnd.raise(true); + continue; + } + + auto &rumble_queue = session->control.rumble_queue; + while (rumble_queue->peek()) { + auto rumble = rumble_queue->pop(); + + send_rumble(session, rumble->id, rumble->lowfreq, rumble->highfreq); + } + + // Unlike rumble which we send as best-effort, HDR state messages are critical + // for proper functioning of some clients. We must wait to pop entries from + // the queue until we're sure we have a peer to send them to. + auto &hdr_queue = session->control.hdr_queue; + while (session->control.peer && hdr_queue->peek()) { + auto hdr_info = hdr_queue->pop(); + + send_hdr_mode(session, std::move(hdr_info)); + } + + ++pos; + }) + } + + if (proc::proc.running() == 0) { + BOOST_LOG(debug) << "Process terminated"sv; + + break; + } + + server->iterate(150ms); + } + + // Let all remaining connections know the server is shutting down + // reason: graceful termination + std::uint32_t reason = 0x80030023; + + control_terminate_t plaintext; + plaintext.header.type = packetTypes[IDX_TERMINATION]; + plaintext.header.payloadLength = sizeof(plaintext.ec); + plaintext.ec = reason; + + std::array + encrypted_payload; + + auto lg = server->_map_addr_session.lock(); + for (auto pos = std::begin(*server->_map_addr_session); pos != std::end(*server->_map_addr_session); ++pos) { + auto session = pos->second.second; + + auto payload = encode_control(session, util::view(plaintext), encrypted_payload); + + if (server->send(payload, session->control.peer)) { + TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session->control.peer->address.address)); + BOOST_LOG(warning) << "Couldn't send termination code to ["sv << addr << ':' << port << ']'; + } + + session->shutdown_event->raise(true); + session->controlEnd.raise(true); + } + + server->flush(); + } + + void + recvThread(broadcast_ctx_t &ctx) { + std::map peer_to_video_session; + std::map peer_to_audio_session; + + auto &video_sock = ctx.video_sock; + auto &audio_sock = ctx.audio_sock; + + auto &message_queue_queue = ctx.message_queue_queue; + auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); + + auto &io = ctx.io; + + udp::endpoint peer; + + std::array buf[2]; + std::function recv_func[2]; + + auto populate_peer_to_session = [&]() { + while (message_queue_queue->peek()) { + auto message_queue_opt = message_queue_queue->pop(); + TUPLE_3D_REF(socket_type, addr, message_queue, *message_queue_opt); + + switch (socket_type) { + case socket_e::video: + if (message_queue) { + peer_to_video_session.emplace(addr, message_queue); + } + else { + peer_to_video_session.erase(addr); + } + break; + case socket_e::audio: + if (message_queue) { + peer_to_audio_session.emplace(addr, message_queue); + } + else { + peer_to_audio_session.erase(addr); + } + break; + } } }; - }; - recv_func_init(video_sock, 0, peer_to_video_session); - recv_func_init(audio_sock, 1, peer_to_audio_session); + auto recv_func_init = [&](udp::socket &sock, int buf_elem, std::map &peer_to_session) { + recv_func[buf_elem] = [&, buf_elem](const boost::system::error_code &ec, size_t bytes) { + auto fg = util::fail_guard([&]() { + sock.async_receive_from(asio::buffer(buf[buf_elem]), peer, 0, recv_func[buf_elem]); + }); - video_sock.async_receive_from(asio::buffer(buf[0]), peer, 0, recv_func[0]); - audio_sock.async_receive_from(asio::buffer(buf[1]), peer, 0, recv_func[1]); + auto type_str = buf_elem ? "AUDIO"sv : "VIDEO"sv; + BOOST_LOG(verbose) << "Recv: "sv << peer.address().to_string() << ':' << peer.port() << " :: " << type_str; - while(!broadcast_shutdown_event->peek()) { - io.run(); - } -} + populate_peer_to_session(); -void videoBroadcastThread(udp::socket &sock) { - auto shutdown_event = mail::man->event(mail::broadcast_shutdown); - auto packets = mail::man->queue(mail::video_packets); - auto timebase = boost::posix_time::microsec_clock::universal_time(); + // No data, yet no error + if (ec == boost::system::errc::connection_refused || ec == boost::system::errc::connection_reset) { + return; + } - // Video traffic is sent on this thread - platf::adjust_thread_priority(platf::thread_priority_e::high); + if (ec || !bytes) { + BOOST_LOG(error) << "Couldn't receive data from udp socket: "sv << ec.message(); + return; + } - while(auto packet = packets->pop()) { - if(shutdown_event->peek()) { - break; + auto it = peer_to_session.find(peer.address()); + if (it != std::end(peer_to_session)) { + BOOST_LOG(debug) << "RAISE: "sv << peer.address().to_string() << ':' << peer.port() << " :: " << type_str; + it->second->raise(peer.port(), std::string { buf[buf_elem].data(), bytes }); + } + }; + }; + + recv_func_init(video_sock, 0, peer_to_video_session); + recv_func_init(audio_sock, 1, peer_to_audio_session); + + video_sock.async_receive_from(asio::buffer(buf[0]), peer, 0, recv_func[0]); + audio_sock.async_receive_from(asio::buffer(buf[1]), peer, 0, recv_func[1]); + + while (!broadcast_shutdown_event->peek()) { + io.run(); } + } - auto session = (session_t *)packet->channel_data; - auto lowseq = session->video.lowseq; + void + videoBroadcastThread(udp::socket &sock) { + auto shutdown_event = mail::man->event(mail::broadcast_shutdown); + auto packets = mail::man->queue(mail::video_packets); + auto timebase = boost::posix_time::microsec_clock::universal_time(); - auto av_packet = packet->av_packet; - std::string_view payload { (char *)av_packet->data, (size_t)av_packet->size }; - std::vector payload_new; + // Video traffic is sent on this thread + platf::adjust_thread_priority(platf::thread_priority_e::high); - video_short_frame_header_t frame_header = {}; - frame_header.headerType = 0x01; // Short header type - frame_header.frameType = (av_packet->flags & AV_PKT_FLAG_KEY) ? 2 : 1; + while (auto packet = packets->pop()) { + if (shutdown_event->peek()) { + break; + } - std::copy_n((uint8_t *)&frame_header, sizeof(frame_header), std::back_inserter(payload_new)); - std::copy(std::begin(payload), std::end(payload), std::back_inserter(payload_new)); + auto session = (session_t *) packet->channel_data; + auto lowseq = session->video.lowseq; - payload = { (char *)payload_new.data(), payload_new.size() }; + auto av_packet = packet->av_packet; + std::string_view payload { (char *) av_packet->data, (size_t) av_packet->size }; + std::vector payload_new; - if(av_packet->flags & AV_PKT_FLAG_KEY) { - for(auto &replacement : *packet->replacements) { - auto frame_old = replacement.old; - auto frame_new = replacement._new; + video_short_frame_header_t frame_header = {}; + frame_header.headerType = 0x01; // Short header type + frame_header.frameType = (av_packet->flags & AV_PKT_FLAG_KEY) ? 2 : 1; - payload_new = replace(payload, frame_old, frame_new); - payload = { (char *)payload_new.data(), payload_new.size() }; + std::copy_n((uint8_t *) &frame_header, sizeof(frame_header), std::back_inserter(payload_new)); + std::copy(std::begin(payload), std::end(payload), std::back_inserter(payload_new)); + + payload = { (char *) payload_new.data(), payload_new.size() }; + + if (av_packet->flags & AV_PKT_FLAG_KEY) { + for (auto &replacement : *packet->replacements) { + auto frame_old = replacement.old; + auto frame_new = replacement._new; + + payload_new = replace(payload, frame_old, frame_new); + payload = { (char *) payload_new.data(), payload_new.size() }; + } + } + + // insert packet headers + auto blocksize = session->config.packetsize + MAX_RTP_HEADER_SIZE; + auto payload_blocksize = blocksize - sizeof(video_packet_raw_t); + + auto fecPercentage = config::stream.fec_percentage; + + payload_new = insert(sizeof(video_packet_raw_t), payload_blocksize, + payload, [&](void *p, int fecIndex, int end) { + video_packet_raw_t *video_packet = (video_packet_raw_t *) p; + + video_packet->packet.flags = FLAG_CONTAINS_PIC_DATA; + }); + + payload = std::string_view { (char *) payload_new.data(), payload_new.size() }; + + // With a fecpercentage of 255, if payload_new is broken up into more than a 100 data_shards + // it will generate greater than DATA_SHARDS_MAX shards. + // Therefore, we start breaking the data up into three separate fec blocks. + auto multi_fec_threshold = 90 * blocksize; + + // We can go up to 4 fec blocks, but 3 is plenty + constexpr auto MAX_FEC_BLOCKS = 3; + + std::array fec_blocks; + decltype(fec_blocks)::iterator + fec_blocks_begin = std::begin(fec_blocks), + fec_blocks_end = std::begin(fec_blocks) + 1; + + auto lastBlockIndex = 0; + if (payload.size() > multi_fec_threshold) { + BOOST_LOG(verbose) << "Generating multiple FEC blocks"sv; + + // Align individual fec blocks to blocksize + auto unaligned_size = payload.size() / MAX_FEC_BLOCKS; + auto aligned_size = ((unaligned_size + (blocksize - 1)) / blocksize) * blocksize; + + // Break the data up into 3 blocks, each containing multiple complete video packets. + fec_blocks[0] = payload.substr(0, aligned_size); + fec_blocks[1] = payload.substr(aligned_size, aligned_size); + fec_blocks[2] = payload.substr(aligned_size * 2); + + lastBlockIndex = 2 << 6; + fec_blocks_end = std::end(fec_blocks); + } + else { + BOOST_LOG(verbose) << "Generating single FEC block"sv; + fec_blocks[0] = payload; + } + + try { + auto blockIndex = 0; + std::for_each(fec_blocks_begin, fec_blocks_end, [&](std::string_view ¤t_payload) { + auto packets = (current_payload.size() + (blocksize - 1)) / blocksize; + + for (int x = 0; x < packets; ++x) { + auto *inspect = (video_packet_raw_t *) ¤t_payload[x * blocksize]; + auto av_packet = packet->av_packet; + + inspect->packet.frameIndex = av_packet->pts; + inspect->packet.streamPacketIndex = ((uint32_t) lowseq + x) << 8; + + // Match multiFecFlags with Moonlight + inspect->packet.multiFecFlags = 0x10; + inspect->packet.multiFecBlocks = (blockIndex << 4) | lastBlockIndex; + + if (x == 0) { + inspect->packet.flags |= FLAG_SOF; + } + + if (x == packets - 1) { + inspect->packet.flags |= FLAG_EOF; + } + } + + auto shards = fec::encode(current_payload, blocksize, fecPercentage, session->config.minRequiredFecPackets); + + // set FEC info now that we know for sure what our percentage will be for this frame + for (auto x = 0; x < shards.size(); ++x) { + auto *inspect = (video_packet_raw_t *) shards.data(x); + + // RTP video timestamps use a 90 KHz clock + auto now = boost::posix_time::microsec_clock::universal_time(); + auto timestamp = (now - timebase).total_microseconds() / (1000 / 90); + + inspect->packet.fecInfo = + (x << 12 | + shards.data_shards << 22 | + shards.percentage << 4); + + inspect->rtp.header = 0x80 | FLAG_EXTENSION; + inspect->rtp.sequenceNumber = util::endian::big(lowseq + x); + inspect->rtp.timestamp = util::endian::big(timestamp); + + inspect->packet.multiFecBlocks = (blockIndex << 4) | lastBlockIndex; + inspect->packet.frameIndex = av_packet->pts; + } + + auto peer_address = session->video.peer.address(); + auto batch_info = platf::batched_send_info_t { + shards.shards.begin(), + shards.blocksize, + shards.nr_shards, + (uintptr_t) sock.native_handle(), + peer_address, + session->video.peer.port(), + }; + + // Use a batched send if it's supported on this platform + if (!platf::send_batch(batch_info)) { + // Batched send is not available, so send each packet individually + BOOST_LOG(verbose) << "Falling back to unbatched send"sv; + for (auto x = 0; x < shards.size(); ++x) { + sock.send_to(asio::buffer(shards[x]), session->video.peer); + } + } + + if (av_packet->flags & AV_PKT_FLAG_KEY) { + BOOST_LOG(verbose) << "Key Frame ["sv << av_packet->pts << "] :: send ["sv << shards.size() << "] shards..."sv; + } + else { + BOOST_LOG(verbose) << "Frame ["sv << av_packet->pts << "] :: send ["sv << shards.size() << "] shards..."sv << std::endl; + } + + ++blockIndex; + lowseq += shards.size(); + }); + + session->video.lowseq = lowseq; + } + catch (const std::exception &e) { + BOOST_LOG(error) << "Broadcast video failed "sv << e.what(); + std::this_thread::sleep_for(100ms); } } - // insert packet headers - auto blocksize = session->config.packetsize + MAX_RTP_HEADER_SIZE; - auto payload_blocksize = blocksize - sizeof(video_packet_raw_t); + shutdown_event->raise(true); + } - auto fecPercentage = config::stream.fec_percentage; + void + audioBroadcastThread(udp::socket &sock) { + auto shutdown_event = mail::man->event(mail::broadcast_shutdown); + auto packets = mail::man->queue(mail::audio_packets); - payload_new = insert(sizeof(video_packet_raw_t), payload_blocksize, - payload, [&](void *p, int fecIndex, int end) { - video_packet_raw_t *video_packet = (video_packet_raw_t *)p; + constexpr auto max_block_size = crypto::cipher::round_to_pkcs7_padded(2048); - video_packet->packet.flags = FLAG_CONTAINS_PIC_DATA; + audio_packet_t audio_packet { (audio_packet_raw_t *) malloc(sizeof(audio_packet_raw_t) + max_block_size) }; + fec::rs_t rs { reed_solomon_new(RTPA_DATA_SHARDS, RTPA_FEC_SHARDS) }; + + // For unknown reasons, the RS parity matrix computed by our RS implementation + // doesn't match the one Nvidia uses for audio data. I'm not exactly sure why, + // but we can simply replace it with the matrix generated by OpenFEC which + // works correctly. This is possible because the data and FEC shard count is + // constant and known in advance. + const unsigned char parity[] = { 0x77, 0x40, 0x38, 0x0e, 0xc7, 0xa7, 0x0d, 0x6c }; + memcpy(rs.get()->p, parity, sizeof(parity)); + + audio_packet->rtp.header = 0x80; + audio_packet->rtp.packetType = 97; + audio_packet->rtp.ssrc = 0; + + // Audio traffic is sent on this thread + platf::adjust_thread_priority(platf::thread_priority_e::high); + + while (auto packet = packets->pop()) { + if (shutdown_event->peek()) { + break; + } + + TUPLE_2D_REF(channel_data, packet_data, *packet); + auto session = (session_t *) channel_data; + + auto sequenceNumber = session->audio.sequenceNumber; + auto timestamp = session->audio.timestamp; + + // This will be mapped to big-endianness later + // For now, encode_audio needs it to be the proper sequenceNumber + audio_packet->rtp.sequenceNumber = sequenceNumber; + + auto bytes = encode_audio(session->config.featureFlags, packet_data, audio_packet, session->audio.avRiKeyId, session->audio.cipher); + if (bytes < 0) { + BOOST_LOG(error) << "Couldn't encode audio packet"sv; + break; + } + + audio_packet->rtp.sequenceNumber = util::endian::big(sequenceNumber); + audio_packet->rtp.timestamp = util::endian::big(timestamp); + + session->audio.sequenceNumber++; + session->audio.timestamp += session->config.audio.packetDuration; + + auto &shards_p = session->audio.shards_p; + + std::copy_n(audio_packet->payload(), bytes, shards_p[sequenceNumber % RTPA_DATA_SHARDS]); + try { + sock.send_to(asio::buffer((char *) audio_packet.get(), sizeof(audio_packet_raw_t) + bytes), session->audio.peer); + + BOOST_LOG(verbose) << "Audio ["sv << sequenceNumber << "] :: send..."sv; + + auto &fec_packet = session->audio.fec_packet; + // initialize the FEC header at the beginning of the FEC block + if (sequenceNumber % RTPA_DATA_SHARDS == 0) { + fec_packet->fecHeader.baseSequenceNumber = util::endian::big(sequenceNumber); + fec_packet->fecHeader.baseTimestamp = util::endian::big(timestamp); + } + + // generate parity shards at the end of the FEC block + if ((sequenceNumber + 1) % RTPA_DATA_SHARDS == 0) { + reed_solomon_encode(rs.get(), shards_p.begin(), RTPA_TOTAL_SHARDS, bytes); + + for (auto x = 0; x < RTPA_FEC_SHARDS; ++x) { + fec_packet->rtp.sequenceNumber = util::endian::big(sequenceNumber + x + 1); + fec_packet->fecHeader.fecShardIndex = x; + memcpy(fec_packet->payload(), shards_p[RTPA_DATA_SHARDS + x], bytes); + sock.send_to(asio::buffer((char *) fec_packet.get(), sizeof(audio_fec_packet_raw_t) + bytes), session->audio.peer); + BOOST_LOG(verbose) << "Audio FEC ["sv << (sequenceNumber & ~(RTPA_DATA_SHARDS - 1)) << ' ' << x << "] :: send..."sv; + } + } + } + catch (const std::exception &e) { + BOOST_LOG(error) << "Broadcast audio failed "sv << e.what(); + std::this_thread::sleep_for(100ms); + } + } + + shutdown_event->raise(true); + } + + int + start_broadcast(broadcast_ctx_t &ctx) { + auto control_port = map_port(CONTROL_PORT); + auto video_port = map_port(VIDEO_STREAM_PORT); + auto audio_port = map_port(AUDIO_STREAM_PORT); + + if (ctx.control_server.bind(control_port)) { + BOOST_LOG(error) << "Couldn't bind Control server to port ["sv << control_port << "], likely another process already bound to the port"sv; + + return -1; + } + + boost::system::error_code ec; + ctx.video_sock.open(udp::v4(), ec); + if (ec) { + BOOST_LOG(fatal) << "Couldn't open socket for Video server: "sv << ec.message(); + + return -1; + } + + ctx.video_sock.bind(udp::endpoint(udp::v4(), video_port), ec); + if (ec) { + BOOST_LOG(fatal) << "Couldn't bind Video server to port ["sv << video_port << "]: "sv << ec.message(); + + return -1; + } + + ctx.audio_sock.open(udp::v4(), ec); + if (ec) { + BOOST_LOG(fatal) << "Couldn't open socket for Audio server: "sv << ec.message(); + + return -1; + } + + ctx.audio_sock.bind(udp::endpoint(udp::v4(), audio_port), ec); + if (ec) { + BOOST_LOG(fatal) << "Couldn't bind Audio server to port ["sv << audio_port << "]: "sv << ec.message(); + + return -1; + } + + ctx.message_queue_queue = std::make_shared(30); + + ctx.video_thread = std::thread { videoBroadcastThread, std::ref(ctx.video_sock) }; + ctx.audio_thread = std::thread { audioBroadcastThread, std::ref(ctx.audio_sock) }; + ctx.control_thread = std::thread { controlBroadcastThread, &ctx.control_server }; + + ctx.recv_thread = std::thread { recvThread, std::ref(ctx) }; + + return 0; + } + + void + end_broadcast(broadcast_ctx_t &ctx) { + auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); + + broadcast_shutdown_event->raise(true); + + auto video_packets = mail::man->queue(mail::video_packets); + auto audio_packets = mail::man->queue(mail::audio_packets); + + // Minimize delay stopping video/audio threads + video_packets->stop(); + audio_packets->stop(); + + ctx.message_queue_queue->stop(); + ctx.io.stop(); + + ctx.video_sock.close(); + ctx.audio_sock.close(); + + video_packets.reset(); + audio_packets.reset(); + + BOOST_LOG(debug) << "Waiting for main listening thread to end..."sv; + ctx.recv_thread.join(); + BOOST_LOG(debug) << "Waiting for main video thread to end..."sv; + ctx.video_thread.join(); + BOOST_LOG(debug) << "Waiting for main audio thread to end..."sv; + ctx.audio_thread.join(); + BOOST_LOG(debug) << "Waiting for main control thread to end..."sv; + ctx.control_thread.join(); + BOOST_LOG(debug) << "All broadcasting threads ended"sv; + + broadcast_shutdown_event->reset(); + } + + int + recv_ping(decltype(broadcast)::ptr_t ref, socket_e type, udp::endpoint &peer, std::chrono::milliseconds timeout) { + auto constexpr ping = "PING"sv; + + auto messages = std::make_shared(30); + ref->message_queue_queue->raise(type, peer.address(), messages); + + auto fg = util::fail_guard([&]() { + messages->stop(); + + // remove message queue from session + ref->message_queue_queue->raise(type, peer.address(), nullptr); + }); + + auto start_time = std::chrono::steady_clock::now(); + auto current_time = start_time; + + while (current_time - start_time < config::stream.ping_timeout) { + auto delta_time = current_time - start_time; + + auto msg_opt = messages->pop(config::stream.ping_timeout - delta_time); + if (!msg_opt) { + break; + } + + TUPLE_2D_REF(port, msg, *msg_opt); + if (msg == ping) { + BOOST_LOG(debug) << "Received ping from "sv << peer.address() << ':' << port << " ["sv << util::hex_vec(msg) << ']'; + + // Update connection details. + { + auto addr_str = peer.address().to_string(); + + auto &connections = ref->audio_video_connections; + + auto lg = connections.lock(); + + std::remove_reference_t::iterator pos = std::end(*connections); + + for (auto it = std::begin(*connections); it != std::end(*connections); ++it) { + TUPLE_2D_REF(addr, port_ref, *it); + + if (!port_ref && addr_str == addr) { + pos = it; + } + else if (port_ref == port) { + break; + } + } + + if (pos == std::end(*connections)) { + continue; + } + + pos->second = port; + peer.port(port); + } + + return port; + } + + BOOST_LOG(debug) << "Received non-ping from "sv << peer.address() << ':' << port << " ["sv << util::hex_vec(msg) << ']'; + + current_time = std::chrono::steady_clock::now(); + } + + BOOST_LOG(error) << "Initial Ping Timeout"sv; + return -1; + } + + void + videoThread(session_t *session) { + auto fg = util::fail_guard([&]() { + session::stop(*session); + }); + + while_starting_do_nothing(session->state); + + auto ref = broadcast.ref(); + auto port = recv_ping(ref, socket_e::video, session->video.peer, config::stream.ping_timeout); + if (port < 0) { + return; + } + + // Enable QoS tagging on video traffic if requested by the client + if (session->config.videoQosType) { + auto address = session->video.peer.address(); + session->video.qos = std::move(platf::enable_socket_qos(ref->video_sock.native_handle(), address, + session->video.peer.port(), platf::qos_data_type_e::video)); + } + + BOOST_LOG(debug) << "Start capturing Video"sv; + video::capture(session->mail, session->config.monitor, session); + } + + void + audioThread(session_t *session) { + auto fg = util::fail_guard([&]() { + session::stop(*session); + }); + + while_starting_do_nothing(session->state); + + auto ref = broadcast.ref(); + auto port = recv_ping(ref, socket_e::audio, session->audio.peer, config::stream.ping_timeout); + if (port < 0) { + return; + } + + // Enable QoS tagging on audio traffic if requested by the client + if (session->config.audioQosType) { + auto address = session->audio.peer.address(); + session->audio.qos = std::move(platf::enable_socket_qos(ref->audio_sock.native_handle(), address, + session->audio.peer.port(), platf::qos_data_type_e::audio)); + } + + BOOST_LOG(debug) << "Start capturing Audio"sv; + audio::capture(session->mail, session->config.audio, session); + } + + namespace session { + std::atomic_uint running_sessions; + + state_e + state(session_t &session) { + return session.state.load(std::memory_order_relaxed); + } + + void + stop(session_t &session) { + while_starting_do_nothing(session.state); + auto expected = state_e::RUNNING; + auto already_stopping = !session.state.compare_exchange_strong(expected, state_e::STOPPING); + if (already_stopping) { + return; + } + + session.shutdown_event->raise(true); + } + + void + join(session_t &session) { + // Current Nvidia drivers have a bug where NVENC can deadlock the encoder thread with hardware-accelerated + // GPU scheduling enabled. If this happens, we will terminate ourselves and the service can restart. + // The alternative is that Sunshine can never start another session until it's manually restarted. + auto task = []() { + BOOST_LOG(fatal) << "Hang detected! Session failed to terminate in 10 seconds."sv; + log_flush(); + std::abort(); + }; + auto force_kill = task_pool.pushDelayed(task, 10s).task_id; + auto fg = util::fail_guard([&force_kill]() { + // Cancel the kill task if we manage to return from this function + task_pool.cancel(force_kill); }); - payload = std::string_view { (char *)payload_new.data(), payload_new.size() }; - - // With a fecpercentage of 255, if payload_new is broken up into more than a 100 data_shards - // it will generate greater than DATA_SHARDS_MAX shards. - // Therefore, we start breaking the data up into three separate fec blocks. - auto multi_fec_threshold = 90 * blocksize; - - // We can go up to 4 fec blocks, but 3 is plenty - constexpr auto MAX_FEC_BLOCKS = 3; - - std::array fec_blocks; - decltype(fec_blocks)::iterator - fec_blocks_begin = std::begin(fec_blocks), - fec_blocks_end = std::begin(fec_blocks) + 1; - - auto lastBlockIndex = 0; - if(payload.size() > multi_fec_threshold) { - BOOST_LOG(verbose) << "Generating multiple FEC blocks"sv; - - // Align individual fec blocks to blocksize - auto unaligned_size = payload.size() / MAX_FEC_BLOCKS; - auto aligned_size = ((unaligned_size + (blocksize - 1)) / blocksize) * blocksize; - - // Break the data up into 3 blocks, each containing multiple complete video packets. - fec_blocks[0] = payload.substr(0, aligned_size); - fec_blocks[1] = payload.substr(aligned_size, aligned_size); - fec_blocks[2] = payload.substr(aligned_size * 2); - - lastBlockIndex = 2 << 6; - fec_blocks_end = std::end(fec_blocks); - } - else { - BOOST_LOG(verbose) << "Generating single FEC block"sv; - fec_blocks[0] = payload; - } - - try { - auto blockIndex = 0; - std::for_each(fec_blocks_begin, fec_blocks_end, [&](std::string_view ¤t_payload) { - auto packets = (current_payload.size() + (blocksize - 1)) / blocksize; - - for(int x = 0; x < packets; ++x) { - auto *inspect = (video_packet_raw_t *)¤t_payload[x * blocksize]; - auto av_packet = packet->av_packet; - - inspect->packet.frameIndex = av_packet->pts; - inspect->packet.streamPacketIndex = ((uint32_t)lowseq + x) << 8; - - // Match multiFecFlags with Moonlight - inspect->packet.multiFecFlags = 0x10; - inspect->packet.multiFecBlocks = (blockIndex << 4) | lastBlockIndex; - - if(x == 0) { - inspect->packet.flags |= FLAG_SOF; - } - - if(x == packets - 1) { - inspect->packet.flags |= FLAG_EOF; - } - } - - auto shards = fec::encode(current_payload, blocksize, fecPercentage, session->config.minRequiredFecPackets); - - // set FEC info now that we know for sure what our percentage will be for this frame - for(auto x = 0; x < shards.size(); ++x) { - auto *inspect = (video_packet_raw_t *)shards.data(x); - - // RTP video timestamps use a 90 KHz clock - auto now = boost::posix_time::microsec_clock::universal_time(); - auto timestamp = (now - timebase).total_microseconds() / (1000 / 90); - - inspect->packet.fecInfo = - (x << 12 | - shards.data_shards << 22 | - shards.percentage << 4); - - inspect->rtp.header = 0x80 | FLAG_EXTENSION; - inspect->rtp.sequenceNumber = util::endian::big(lowseq + x); - inspect->rtp.timestamp = util::endian::big(timestamp); - - inspect->packet.multiFecBlocks = (blockIndex << 4) | lastBlockIndex; - inspect->packet.frameIndex = av_packet->pts; - } - - auto peer_address = session->video.peer.address(); - auto batch_info = platf::batched_send_info_t { - shards.shards.begin(), - shards.blocksize, - shards.nr_shards, - (uintptr_t)sock.native_handle(), - peer_address, - session->video.peer.port(), - }; - - // Use a batched send if it's supported on this platform - if(!platf::send_batch(batch_info)) { - // Batched send is not available, so send each packet individually - BOOST_LOG(verbose) << "Falling back to unbatched send"sv; - for(auto x = 0; x < shards.size(); ++x) { - sock.send_to(asio::buffer(shards[x]), session->video.peer); - } - } - - if(av_packet->flags & AV_PKT_FLAG_KEY) { - BOOST_LOG(verbose) << "Key Frame ["sv << av_packet->pts << "] :: send ["sv << shards.size() << "] shards..."sv; - } - else { - BOOST_LOG(verbose) << "Frame ["sv << av_packet->pts << "] :: send ["sv << shards.size() << "] shards..."sv << std::endl; - } - - ++blockIndex; - lowseq += shards.size(); - }); - - session->video.lowseq = lowseq; - } - catch(const std::exception &e) { - BOOST_LOG(error) << "Broadcast video failed "sv << e.what(); - std::this_thread::sleep_for(100ms); - } - } - - shutdown_event->raise(true); -} - -void audioBroadcastThread(udp::socket &sock) { - auto shutdown_event = mail::man->event(mail::broadcast_shutdown); - auto packets = mail::man->queue(mail::audio_packets); - - constexpr auto max_block_size = crypto::cipher::round_to_pkcs7_padded(2048); - - audio_packet_t audio_packet { (audio_packet_raw_t *)malloc(sizeof(audio_packet_raw_t) + max_block_size) }; - fec::rs_t rs { reed_solomon_new(RTPA_DATA_SHARDS, RTPA_FEC_SHARDS) }; - - // For unknown reasons, the RS parity matrix computed by our RS implementation - // doesn't match the one Nvidia uses for audio data. I'm not exactly sure why, - // but we can simply replace it with the matrix generated by OpenFEC which - // works correctly. This is possible because the data and FEC shard count is - // constant and known in advance. - const unsigned char parity[] = { 0x77, 0x40, 0x38, 0x0e, 0xc7, 0xa7, 0x0d, 0x6c }; - memcpy(rs.get()->p, parity, sizeof(parity)); - - audio_packet->rtp.header = 0x80; - audio_packet->rtp.packetType = 97; - audio_packet->rtp.ssrc = 0; - - // Audio traffic is sent on this thread - platf::adjust_thread_priority(platf::thread_priority_e::high); - - while(auto packet = packets->pop()) { - if(shutdown_event->peek()) { - break; - } - - TUPLE_2D_REF(channel_data, packet_data, *packet); - auto session = (session_t *)channel_data; - - auto sequenceNumber = session->audio.sequenceNumber; - auto timestamp = session->audio.timestamp; - - // This will be mapped to big-endianness later - // For now, encode_audio needs it to be the proper sequenceNumber - audio_packet->rtp.sequenceNumber = sequenceNumber; - - auto bytes = encode_audio(session->config.featureFlags, packet_data, audio_packet, session->audio.avRiKeyId, session->audio.cipher); - if(bytes < 0) { - BOOST_LOG(error) << "Couldn't encode audio packet"sv; - break; - } - - audio_packet->rtp.sequenceNumber = util::endian::big(sequenceNumber); - audio_packet->rtp.timestamp = util::endian::big(timestamp); - - session->audio.sequenceNumber++; - session->audio.timestamp += session->config.audio.packetDuration; - - auto &shards_p = session->audio.shards_p; - - std::copy_n(audio_packet->payload(), bytes, shards_p[sequenceNumber % RTPA_DATA_SHARDS]); - try { - sock.send_to(asio::buffer((char *)audio_packet.get(), sizeof(audio_packet_raw_t) + bytes), session->audio.peer); - - - BOOST_LOG(verbose) << "Audio ["sv << sequenceNumber << "] :: send..."sv; - - auto &fec_packet = session->audio.fec_packet; - // initialize the FEC header at the beginning of the FEC block - if(sequenceNumber % RTPA_DATA_SHARDS == 0) { - fec_packet->fecHeader.baseSequenceNumber = util::endian::big(sequenceNumber); - fec_packet->fecHeader.baseTimestamp = util::endian::big(timestamp); - } - - // generate parity shards at the end of the FEC block - if((sequenceNumber + 1) % RTPA_DATA_SHARDS == 0) { - reed_solomon_encode(rs.get(), shards_p.begin(), RTPA_TOTAL_SHARDS, bytes); - - for(auto x = 0; x < RTPA_FEC_SHARDS; ++x) { - fec_packet->rtp.sequenceNumber = util::endian::big(sequenceNumber + x + 1); - fec_packet->fecHeader.fecShardIndex = x; - memcpy(fec_packet->payload(), shards_p[RTPA_DATA_SHARDS + x], bytes); - sock.send_to(asio::buffer((char *)fec_packet.get(), sizeof(audio_fec_packet_raw_t) + bytes), session->audio.peer); - BOOST_LOG(verbose) << "Audio FEC ["sv << (sequenceNumber & ~(RTPA_DATA_SHARDS - 1)) << ' ' << x << "] :: send..."sv; - } - } - } - catch(const std::exception &e) { - BOOST_LOG(error) << "Broadcast audio failed "sv << e.what(); - std::this_thread::sleep_for(100ms); - } - } - - shutdown_event->raise(true); -} - -int start_broadcast(broadcast_ctx_t &ctx) { - auto control_port = map_port(CONTROL_PORT); - auto video_port = map_port(VIDEO_STREAM_PORT); - auto audio_port = map_port(AUDIO_STREAM_PORT); - - if(ctx.control_server.bind(control_port)) { - BOOST_LOG(error) << "Couldn't bind Control server to port ["sv << control_port << "], likely another process already bound to the port"sv; - - return -1; - } - - boost::system::error_code ec; - ctx.video_sock.open(udp::v4(), ec); - if(ec) { - BOOST_LOG(fatal) << "Couldn't open socket for Video server: "sv << ec.message(); - - return -1; - } - - ctx.video_sock.bind(udp::endpoint(udp::v4(), video_port), ec); - if(ec) { - BOOST_LOG(fatal) << "Couldn't bind Video server to port ["sv << video_port << "]: "sv << ec.message(); - - return -1; - } - - ctx.audio_sock.open(udp::v4(), ec); - if(ec) { - BOOST_LOG(fatal) << "Couldn't open socket for Audio server: "sv << ec.message(); - - return -1; - } - - ctx.audio_sock.bind(udp::endpoint(udp::v4(), audio_port), ec); - if(ec) { - BOOST_LOG(fatal) << "Couldn't bind Audio server to port ["sv << audio_port << "]: "sv << ec.message(); - - return -1; - } - - ctx.message_queue_queue = std::make_shared(30); - - ctx.video_thread = std::thread { videoBroadcastThread, std::ref(ctx.video_sock) }; - ctx.audio_thread = std::thread { audioBroadcastThread, std::ref(ctx.audio_sock) }; - ctx.control_thread = std::thread { controlBroadcastThread, &ctx.control_server }; - - ctx.recv_thread = std::thread { recvThread, std::ref(ctx) }; - - return 0; -} - -void end_broadcast(broadcast_ctx_t &ctx) { - auto broadcast_shutdown_event = mail::man->event(mail::broadcast_shutdown); - - broadcast_shutdown_event->raise(true); - - auto video_packets = mail::man->queue(mail::video_packets); - auto audio_packets = mail::man->queue(mail::audio_packets); - - // Minimize delay stopping video/audio threads - video_packets->stop(); - audio_packets->stop(); - - ctx.message_queue_queue->stop(); - ctx.io.stop(); - - ctx.video_sock.close(); - ctx.audio_sock.close(); - - video_packets.reset(); - audio_packets.reset(); - - BOOST_LOG(debug) << "Waiting for main listening thread to end..."sv; - ctx.recv_thread.join(); - BOOST_LOG(debug) << "Waiting for main video thread to end..."sv; - ctx.video_thread.join(); - BOOST_LOG(debug) << "Waiting for main audio thread to end..."sv; - ctx.audio_thread.join(); - BOOST_LOG(debug) << "Waiting for main control thread to end..."sv; - ctx.control_thread.join(); - BOOST_LOG(debug) << "All broadcasting threads ended"sv; - - broadcast_shutdown_event->reset(); -} - -int recv_ping(decltype(broadcast)::ptr_t ref, socket_e type, udp::endpoint &peer, std::chrono::milliseconds timeout) { - auto constexpr ping = "PING"sv; - - auto messages = std::make_shared(30); - ref->message_queue_queue->raise(type, peer.address(), messages); - - auto fg = util::fail_guard([&]() { - messages->stop(); - - // remove message queue from session - ref->message_queue_queue->raise(type, peer.address(), nullptr); - }); - - auto start_time = std::chrono::steady_clock::now(); - auto current_time = start_time; - - while(current_time - start_time < config::stream.ping_timeout) { - auto delta_time = current_time - start_time; - - auto msg_opt = messages->pop(config::stream.ping_timeout - delta_time); - if(!msg_opt) { - break; - } - - TUPLE_2D_REF(port, msg, *msg_opt); - if(msg == ping) { - BOOST_LOG(debug) << "Received ping from "sv << peer.address() << ':' << port << " ["sv << util::hex_vec(msg) << ']'; - - // Update connection details. + BOOST_LOG(debug) << "Waiting for video to end..."sv; + session.videoThread.join(); + BOOST_LOG(debug) << "Waiting for audio to end..."sv; + session.audioThread.join(); + BOOST_LOG(debug) << "Waiting for control to end..."sv; + session.controlEnd.view(); + // Reset input on session stop to avoid stuck repeated keys + BOOST_LOG(debug) << "Resetting Input..."sv; + input::reset(session.input); + + BOOST_LOG(debug) << "Removing references to any connections..."sv; { - auto addr_str = peer.address().to_string(); + auto video_addr = session.video.peer.address().to_string(); + auto audio_addr = session.audio.peer.address().to_string(); - auto &connections = ref->audio_video_connections; + auto video_port = session.video.peer.port(); + auto audio_port = session.audio.peer.port(); + + auto &connections = session.broadcast_ref->audio_video_connections; auto lg = connections.lock(); - std::remove_reference_t::iterator pos = std::end(*connections); + auto validate_size = connections->size(); + for (auto it = std::begin(*connections); it != std::end(*connections);) { + TUPLE_2D_REF(addr, port, *it); - for(auto it = std::begin(*connections); it != std::end(*connections); ++it) { - TUPLE_2D_REF(addr, port_ref, *it); - - if(!port_ref && addr_str == addr) { - pos = it; + if ((video_port == port && video_addr == addr) || + (audio_port == port && audio_addr == addr)) { + it = connections->erase(it); } - else if(port_ref == port) { - break; + else { + ++it; } } - if(pos == std::end(*connections)) { - continue; + auto new_size = connections->size(); + if (validate_size != new_size + 2) { + BOOST_LOG(warning) << "Couldn't remove reference to session connections: ending all broadcasts"sv; + + // A reference to the event object is still stored somewhere else. So no need to keep + // a reference to it. + mail::man->event(mail::broadcast_shutdown)->raise(true); } - - pos->second = port; - peer.port(port); } - return port; - } - - BOOST_LOG(debug) << "Received non-ping from "sv << peer.address() << ':' << port << " ["sv << util::hex_vec(msg) << ']'; - - current_time = std::chrono::steady_clock::now(); - } - - BOOST_LOG(error) << "Initial Ping Timeout"sv; - return -1; -} - -void videoThread(session_t *session) { - auto fg = util::fail_guard([&]() { - session::stop(*session); - }); - - while_starting_do_nothing(session->state); - - auto ref = broadcast.ref(); - auto port = recv_ping(ref, socket_e::video, session->video.peer, config::stream.ping_timeout); - if(port < 0) { - return; - } - - // Enable QoS tagging on video traffic if requested by the client - if(session->config.videoQosType) { - auto address = session->video.peer.address(); - session->video.qos = std::move(platf::enable_socket_qos(ref->video_sock.native_handle(), address, - session->video.peer.port(), platf::qos_data_type_e::video)); - } - - BOOST_LOG(debug) << "Start capturing Video"sv; - video::capture(session->mail, session->config.monitor, session); -} - -void audioThread(session_t *session) { - auto fg = util::fail_guard([&]() { - session::stop(*session); - }); - - while_starting_do_nothing(session->state); - - auto ref = broadcast.ref(); - auto port = recv_ping(ref, socket_e::audio, session->audio.peer, config::stream.ping_timeout); - if(port < 0) { - return; - } - - // Enable QoS tagging on audio traffic if requested by the client - if(session->config.audioQosType) { - auto address = session->audio.peer.address(); - session->audio.qos = std::move(platf::enable_socket_qos(ref->audio_sock.native_handle(), address, - session->audio.peer.port(), platf::qos_data_type_e::audio)); - } - - BOOST_LOG(debug) << "Start capturing Audio"sv; - audio::capture(session->mail, session->config.audio, session); -} - -namespace session { -std::atomic_uint running_sessions; - -state_e state(session_t &session) { - return session.state.load(std::memory_order_relaxed); -} - -void stop(session_t &session) { - while_starting_do_nothing(session.state); - auto expected = state_e::RUNNING; - auto already_stopping = !session.state.compare_exchange_strong(expected, state_e::STOPPING); - if(already_stopping) { - return; - } - - session.shutdown_event->raise(true); -} - -void join(session_t &session) { - // Current Nvidia drivers have a bug where NVENC can deadlock the encoder thread with hardware-accelerated - // GPU scheduling enabled. If this happens, we will terminate ourselves and the service can restart. - // The alternative is that Sunshine can never start another session until it's manually restarted. - auto task = []() { - BOOST_LOG(fatal) << "Hang detected! Session failed to terminate in 10 seconds."sv; - log_flush(); - std::abort(); - }; - auto force_kill = task_pool.pushDelayed(task, 10s).task_id; - auto fg = util::fail_guard([&force_kill]() { - // Cancel the kill task if we manage to return from this function - task_pool.cancel(force_kill); - }); - - BOOST_LOG(debug) << "Waiting for video to end..."sv; - session.videoThread.join(); - BOOST_LOG(debug) << "Waiting for audio to end..."sv; - session.audioThread.join(); - BOOST_LOG(debug) << "Waiting for control to end..."sv; - session.controlEnd.view(); - // Reset input on session stop to avoid stuck repeated keys - BOOST_LOG(debug) << "Resetting Input..."sv; - input::reset(session.input); - - BOOST_LOG(debug) << "Removing references to any connections..."sv; - { - auto video_addr = session.video.peer.address().to_string(); - auto audio_addr = session.audio.peer.address().to_string(); - - auto video_port = session.video.peer.port(); - auto audio_port = session.audio.peer.port(); - - auto &connections = session.broadcast_ref->audio_video_connections; - - auto lg = connections.lock(); - - auto validate_size = connections->size(); - for(auto it = std::begin(*connections); it != std::end(*connections);) { - TUPLE_2D_REF(addr, port, *it); - - if((video_port == port && video_addr == addr) || - (audio_port == port && audio_addr == addr)) { - it = connections->erase(it); + // If this is the last session, invoke the platform callbacks + if (--running_sessions == 0) { + platf::streaming_will_stop(); } - else { - ++it; + + BOOST_LOG(debug) << "Session ended"sv; + } + + int + start(session_t &session, const std::string &addr_string) { + session.input = input::alloc(session.mail); + + session.broadcast_ref = broadcast.ref(); + if (!session.broadcast_ref) { + return -1; } + + session.broadcast_ref->control_server.emplace_addr_to_session(addr_string, session); + + auto addr = boost::asio::ip::make_address(addr_string); + session.video.peer.address(addr); + session.video.peer.port(0); + + session.audio.peer.address(addr); + session.audio.peer.port(0); + + { + auto &connections = session.broadcast_ref->audio_video_connections; + + auto lg = connections.lock(); + + // allocate a location for connections + connections->emplace_back(addr_string, 0); + connections->emplace_back(addr_string, 0); + } + + session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; + + session.audioThread = std::thread { audioThread, &session }; + session.videoThread = std::thread { videoThread, &session }; + + session.state.store(state_e::RUNNING, std::memory_order_relaxed); + + // If this is the first session, invoke the platform callbacks + if (++running_sessions == 1) { + platf::streaming_will_start(); + } + + return 0; } - auto new_size = connections->size(); - if(validate_size != new_size + 2) { - BOOST_LOG(warning) << "Couldn't remove reference to session connections: ending all broadcasts"sv; + std::shared_ptr + alloc(config_t &config, crypto::aes_t &gcm_key, crypto::aes_t &iv) { + auto session = std::make_shared(); - // A reference to the event object is still stored somewhere else. So no need to keep - // a reference to it. - mail::man->event(mail::broadcast_shutdown)->raise(true); + auto mail = std::make_shared(); + + session->shutdown_event = mail->event(mail::shutdown); + + session->config = config; + + session->control.rumble_queue = mail->queue(mail::rumble); + session->control.hdr_queue = mail->event(mail::hdr); + session->control.iv = iv; + session->control.cipher = crypto::cipher::gcm_t { + gcm_key, false + }; + + session->video.idr_events = mail->event(mail::idr); + session->video.lowseq = 0; + + constexpr auto max_block_size = crypto::cipher::round_to_pkcs7_padded(2048); + + util::buffer_t shards { RTPA_TOTAL_SHARDS * max_block_size }; + util::buffer_t shards_p { RTPA_TOTAL_SHARDS }; + + for (auto x = 0; x < RTPA_TOTAL_SHARDS; ++x) { + shards_p[x] = (uint8_t *) &shards[x * max_block_size]; + } + + // Audio FEC spans multiple audio packets, + // therefore its session specific + session->audio.shards = std::move(shards); + session->audio.shards_p = std::move(shards_p); + + session->audio.fec_packet.reset((audio_fec_packet_raw_t *) malloc(sizeof(audio_fec_packet_raw_t) + max_block_size)); + + session->audio.fec_packet->rtp.header = 0x80; + session->audio.fec_packet->rtp.packetType = 127; + session->audio.fec_packet->rtp.timestamp = 0; + session->audio.fec_packet->rtp.ssrc = 0; + + session->audio.fec_packet->fecHeader.payloadType = 97; + session->audio.fec_packet->fecHeader.ssrc = 0; + + session->audio.cipher = crypto::cipher::cbc_t { + gcm_key, true + }; + + session->audio.avRiKeyId = util::endian::big(*(std::uint32_t *) iv.data()); + session->audio.sequenceNumber = 0; + session->audio.timestamp = 0; + + session->control.peer = nullptr; + session->state.store(state_e::STOPPED, std::memory_order_relaxed); + + session->mail = std::move(mail); + + return session; } - } - - // If this is the last session, invoke the platform callbacks - if(--running_sessions == 0) { - platf::streaming_will_stop(); - } - - BOOST_LOG(debug) << "Session ended"sv; -} - -int start(session_t &session, const std::string &addr_string) { - session.input = input::alloc(session.mail); - - session.broadcast_ref = broadcast.ref(); - if(!session.broadcast_ref) { - return -1; - } - - session.broadcast_ref->control_server.emplace_addr_to_session(addr_string, session); - - auto addr = boost::asio::ip::make_address(addr_string); - session.video.peer.address(addr); - session.video.peer.port(0); - - session.audio.peer.address(addr); - session.audio.peer.port(0); - - { - auto &connections = session.broadcast_ref->audio_video_connections; - - auto lg = connections.lock(); - - // allocate a location for connections - connections->emplace_back(addr_string, 0); - connections->emplace_back(addr_string, 0); - } - - - session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; - - session.audioThread = std::thread { audioThread, &session }; - session.videoThread = std::thread { videoThread, &session }; - - session.state.store(state_e::RUNNING, std::memory_order_relaxed); - - // If this is the first session, invoke the platform callbacks - if(++running_sessions == 1) { - platf::streaming_will_start(); - } - - return 0; -} - -std::shared_ptr alloc(config_t &config, crypto::aes_t &gcm_key, crypto::aes_t &iv) { - auto session = std::make_shared(); - - auto mail = std::make_shared(); - - session->shutdown_event = mail->event(mail::shutdown); - - session->config = config; - - session->control.rumble_queue = mail->queue(mail::rumble); - session->control.hdr_queue = mail->event(mail::hdr); - session->control.iv = iv; - session->control.cipher = crypto::cipher::gcm_t { - gcm_key, false - }; - - session->video.idr_events = mail->event(mail::idr); - session->video.lowseq = 0; - - constexpr auto max_block_size = crypto::cipher::round_to_pkcs7_padded(2048); - - util::buffer_t shards { RTPA_TOTAL_SHARDS * max_block_size }; - util::buffer_t shards_p { RTPA_TOTAL_SHARDS }; - - for(auto x = 0; x < RTPA_TOTAL_SHARDS; ++x) { - shards_p[x] = (uint8_t *)&shards[x * max_block_size]; - } - - // Audio FEC spans multiple audio packets, - // therefore its session specific - session->audio.shards = std::move(shards); - session->audio.shards_p = std::move(shards_p); - - session->audio.fec_packet.reset((audio_fec_packet_raw_t *)malloc(sizeof(audio_fec_packet_raw_t) + max_block_size)); - - session->audio.fec_packet->rtp.header = 0x80; - session->audio.fec_packet->rtp.packetType = 127; - session->audio.fec_packet->rtp.timestamp = 0; - session->audio.fec_packet->rtp.ssrc = 0; - - session->audio.fec_packet->fecHeader.payloadType = 97; - session->audio.fec_packet->fecHeader.ssrc = 0; - - session->audio.cipher = crypto::cipher::cbc_t { - gcm_key, true - }; - - session->audio.avRiKeyId = util::endian::big(*(std::uint32_t *)iv.data()); - session->audio.sequenceNumber = 0; - session->audio.timestamp = 0; - - session->control.peer = nullptr; - session->state.store(state_e::STOPPED, std::memory_order_relaxed); - - session->mail = std::move(mail); - - return session; -} -} // namespace session -} // namespace stream + } // namespace session +} // namespace stream diff --git a/src/stream.h b/src/stream.h index 02b30413..c8803f3c 100644 --- a/src/stream.h +++ b/src/stream.h @@ -10,39 +10,44 @@ #include "video.h" namespace stream { -constexpr auto VIDEO_STREAM_PORT = 9; -constexpr auto CONTROL_PORT = 10; -constexpr auto AUDIO_STREAM_PORT = 11; + constexpr auto VIDEO_STREAM_PORT = 9; + constexpr auto CONTROL_PORT = 10; + constexpr auto AUDIO_STREAM_PORT = 11; -struct session_t; -struct config_t { - audio::config_t audio; - video::config_t monitor; + struct session_t; + struct config_t { + audio::config_t audio; + video::config_t monitor; - int packetsize; - int minRequiredFecPackets; - int featureFlags; - int controlProtocolType; - int audioQosType; - int videoQosType; + int packetsize; + int minRequiredFecPackets; + int featureFlags; + int controlProtocolType; + int audioQosType; + int videoQosType; - std::optional gcmap; -}; + std::optional gcmap; + }; -namespace session { -enum class state_e : int { - STOPPED, - STOPPING, - STARTING, - RUNNING, -}; + namespace session { + enum class state_e : int { + STOPPED, + STOPPING, + STARTING, + RUNNING, + }; -std::shared_ptr alloc(config_t &config, crypto::aes_t &gcm_key, crypto::aes_t &iv); -int start(session_t &session, const std::string &addr_string); -void stop(session_t &session); -void join(session_t &session); -state_e state(session_t &session); -} // namespace session -} // namespace stream + std::shared_ptr + alloc(config_t &config, crypto::aes_t &gcm_key, crypto::aes_t &iv); + int + start(session_t &session, const std::string &addr_string); + void + stop(session_t &session); + void + join(session_t &session); + state_e + state(session_t &session); + } // namespace session +} // namespace stream -#endif // SUNSHINE_STREAM_H +#endif // SUNSHINE_STREAM_H diff --git a/src/sync.h b/src/sync.h index 4e7f9427..9deeaa27 100644 --- a/src/sync.h +++ b/src/sync.h @@ -9,85 +9,94 @@ namespace sync_util { -template -class sync_t { -public: - using value_t = T; - using mutex_t = M; + template + class sync_t { + public: + using value_t = T; + using mutex_t = M; - std::lock_guard lock() { - return std::lock_guard { _lock }; - } + std::lock_guard + lock() { + return std::lock_guard { _lock }; + } - template - sync_t(Args &&...args) : raw { std::forward(args)... } {} + template + sync_t(Args &&...args): + raw { std::forward(args)... } {} - sync_t &operator=(sync_t &&other) noexcept { - std::lock(_lock, other._lock); + sync_t & + operator=(sync_t &&other) noexcept { + std::lock(_lock, other._lock); - raw = std::move(other.raw); + raw = std::move(other.raw); - _lock.unlock(); - other._lock.unlock(); + _lock.unlock(); + other._lock.unlock(); - return *this; - } + return *this; + } - sync_t &operator=(sync_t &other) noexcept { - std::lock(_lock, other._lock); + sync_t & + operator=(sync_t &other) noexcept { + std::lock(_lock, other._lock); - raw = other.raw; + raw = other.raw; - _lock.unlock(); - other._lock.unlock(); + _lock.unlock(); + other._lock.unlock(); - return *this; - } + return *this; + } - template - sync_t &operator=(V &&val) { - auto lg = lock(); + template + sync_t & + operator=(V &&val) { + auto lg = lock(); - raw = val; + raw = val; - return *this; - } + return *this; + } - sync_t &operator=(const value_t &val) noexcept { - auto lg = lock(); + sync_t & + operator=(const value_t &val) noexcept { + auto lg = lock(); - raw = val; + raw = val; - return *this; - } + return *this; + } - sync_t &operator=(value_t &&val) noexcept { - auto lg = lock(); + sync_t & + operator=(value_t &&val) noexcept { + auto lg = lock(); - raw = std::move(val); + raw = std::move(val); - return *this; - } + return *this; + } - value_t *operator->() { - return &raw; - } + value_t * + operator->() { + return &raw; + } - value_t &operator*() { - return raw; - } + value_t & + operator*() { + return raw; + } - const value_t &operator*() const { - return raw; - } + const value_t & + operator*() const { + return raw; + } - value_t raw; + value_t raw; -private: - mutex_t _lock; -}; + private: + mutex_t _lock; + }; -} // namespace sync_util +} // namespace sync_util - -#endif // SUNSHINE_SYNC_H +#endif // SUNSHINE_SYNC_H diff --git a/src/system_tray.cpp b/src/system_tray.cpp index 12247c4a..57175bd0 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -4,200 +4,209 @@ // macros #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 -#if defined(_WIN32) || defined(_WIN64) -#define TRAY_ICON WEB_DIR "images/favicon.ico" -#elif defined(__linux__) || defined(linux) || defined(__linux) -#define TRAY_ICON "sunshine" -#elif defined(__APPLE__) || defined(__MACH__) -#define TRAY_ICON WEB_DIR "images/logo-sunshine-16.png" -#include -#endif + #if defined(_WIN32) || defined(_WIN64) + #define TRAY_ICON WEB_DIR "images/favicon.ico" + #elif defined(__linux__) || defined(linux) || defined(__linux) + #define TRAY_ICON "sunshine" + #elif defined(__APPLE__) || defined(__MACH__) + #define TRAY_ICON WEB_DIR "images/logo-sunshine-16.png" + #include + #endif -// standard includes -#include -#include + // standard includes + #include + #include -// lib includes -#include "tray/tray.h" -#include -#include + // lib includes + #include "tray/tray.h" + #include + #include -// local includes -#include "confighttp.h" -#include "main.h" -#include "platform/common.h" -#include "process.h" + // local includes + #include "confighttp.h" + #include "main.h" + #include "platform/common.h" + #include "process.h" using namespace std::literals; // system_tray namespace namespace system_tray { -/** + /** * @brief Open a url in the default web browser. * @param url The url to open. */ -void open_url(const std::string &url) { - boost::filesystem::path working_dir; + void + open_url(const std::string &url) { + boost::filesystem::path working_dir; - // if windows -#if defined(_WIN32) || defined(_WIN64) - // set working dir to Windows system directory - working_dir = boost::filesystem::path(std::getenv("SystemRoot")); + // if windows + #if defined(_WIN32) || defined(_WIN64) + // set working dir to Windows system directory + working_dir = boost::filesystem::path(std::getenv("SystemRoot")); - // this isn't ideal as it briefly shows a command window - // but start a command built into cmd, not an executable - std::string cmd = R"(cmd /C "start )" + url + R"(")"; -#else // if unix - // set working dir to user home directory - working_dir = boost::filesystem::path(std::getenv("HOME")); - std::string cmd = R"(open ")" + url + R"(")"; -#endif + // this isn't ideal as it briefly shows a command window + // but start a command built into cmd, not an executable + std::string cmd = R"(cmd /C "start )" + url + R"(")"; + #else // if unix + // set working dir to user home directory + working_dir = boost::filesystem::path(std::getenv("HOME")); + std::string cmd = R"(open ")" + url + R"(")"; + #endif - boost::process::environment _env = boost::this_process::environment(); - std::error_code ec; - auto child = platf::run_unprivileged(cmd, working_dir, _env, nullptr, ec, nullptr); - if(ec) { - BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); + boost::process::environment _env = boost::this_process::environment(); + std::error_code ec; + auto child = platf::run_unprivileged(cmd, working_dir, _env, nullptr, ec, nullptr); + if (ec) { + BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); + } + else { + BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; + child.detach(); + } } - else { - BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; - child.detach(); - } -} -/** + /** * @brief Callback for opening the UI from the system tray. * @param item The tray menu item. */ -void tray_open_ui_cb(struct tray_menu *item) { - BOOST_LOG(info) << "Opening UI from system tray"sv; + void + tray_open_ui_cb(struct tray_menu *item) { + BOOST_LOG(info) << "Opening UI from system tray"sv; - // create the url with the port - std::string url = "https://localhost:" + std::to_string(map_port(confighttp::PORT_HTTPS)); + // create the url with the port + std::string url = "https://localhost:" + std::to_string(map_port(confighttp::PORT_HTTPS)); - // open the url in the default web browser - open_url(url); -} + // open the url in the default web browser + open_url(url); + } -/** + /** * @brief Callback for opening GitHub Sponsors from the system tray. * @param item The tray menu item. */ -void tray_donate_github_cb(struct tray_menu *item) { - open_url("https://github.com/sponsors/LizardByte"); -} + void + tray_donate_github_cb(struct tray_menu *item) { + open_url("https://github.com/sponsors/LizardByte"); + } -/** + /** * @brief Callback for opening MEE6 donation from the system tray. * @param item The tray menu item. */ -void tray_donate_mee6_cb(struct tray_menu *item) { - open_url("https://mee6.xyz/m/804382334370578482"); -} + void + tray_donate_mee6_cb(struct tray_menu *item) { + open_url("https://mee6.xyz/m/804382334370578482"); + } -/** + /** * @brief Callback for opening Patreon from the system tray. * @param item The tray menu item. */ -void tray_donate_patreon_cb(struct tray_menu *item) { - open_url("https://www.patreon.com/LizardByte"); -} + void + tray_donate_patreon_cb(struct tray_menu *item) { + open_url("https://www.patreon.com/LizardByte"); + } -/** + /** * @brief Callback for opening PayPal donation from the system tray. * @param item The tray menu item. */ -void tray_donate_paypal_cb(struct tray_menu *item) { - open_url("https://www.paypal.com/paypalme/ReenigneArcher"); -} + void + tray_donate_paypal_cb(struct tray_menu *item) { + open_url("https://www.paypal.com/paypalme/ReenigneArcher"); + } -/** + /** * @brief Callback for exiting Sunshine from the system tray. * @param item The tray menu item. */ -void tray_quit_cb(struct tray_menu *item) { - BOOST_LOG(info) << "Quiting from system tray"sv; + void + tray_quit_cb(struct tray_menu *item) { + BOOST_LOG(info) << "Quiting from system tray"sv; - std::raise(SIGINT); -} + std::raise(SIGINT); + } -// Tray menu -static struct tray tray = { - .icon = TRAY_ICON, -#if defined(_WIN32) || defined(_WIN64) - .tooltip = const_cast("Sunshine"), // cast the string literal to a non-const char* pointer -#endif - .menu = - (struct tray_menu[]) { - // todo - use boost/locale to translate menu strings - { .text = "Open Sunshine", .cb = tray_open_ui_cb }, - { .text = "-" }, - { .text = "Donate", - .submenu = - (struct tray_menu[]) { - { .text = "GitHub Sponsors", .cb = tray_donate_github_cb }, - { .text = "MEE6", .cb = tray_donate_mee6_cb }, - { .text = "Patreon", .cb = tray_donate_patreon_cb }, - { .text = "PayPal", .cb = tray_donate_paypal_cb }, - { .text = nullptr } } }, - { .text = "-" }, - { .text = "Quit", .cb = tray_quit_cb }, - { .text = nullptr } }, -}; + // Tray menu + static struct tray tray = { + .icon = TRAY_ICON, + #if defined(_WIN32) || defined(_WIN64) + .tooltip = const_cast("Sunshine"), // cast the string literal to a non-const char* pointer + #endif + .menu = + (struct tray_menu[]) { + // todo - use boost/locale to translate menu strings + { .text = "Open Sunshine", .cb = tray_open_ui_cb }, + { .text = "-" }, + { .text = "Donate", + .submenu = + (struct tray_menu[]) { + { .text = "GitHub Sponsors", .cb = tray_donate_github_cb }, + { .text = "MEE6", .cb = tray_donate_mee6_cb }, + { .text = "Patreon", .cb = tray_donate_patreon_cb }, + { .text = "PayPal", .cb = tray_donate_paypal_cb }, + { .text = nullptr } } }, + { .text = "-" }, + { .text = "Quit", .cb = tray_quit_cb }, + { .text = nullptr } }, + }; - -/** + /** * @brief Create the system tray. * @details This function has an endless loop, so it should be run in a separate thread. * @return 1 if the system tray failed to create, otherwise 0 once the tray has been terminated. */ -int system_tray() { - if(tray_init(&tray) < 0) { - BOOST_LOG(warning) << "Failed to create system tray"sv; - return 1; - } - else { - BOOST_LOG(info) << "System tray created"sv; + int + system_tray() { + if (tray_init(&tray) < 0) { + BOOST_LOG(warning) << "Failed to create system tray"sv; + return 1; + } + else { + BOOST_LOG(info) << "System tray created"sv; + } + + while (tray_loop(1) == 0) { + BOOST_LOG(debug) << "System tray loop"sv; + } + + return 0; } - while(tray_loop(1) == 0) { - BOOST_LOG(debug) << "System tray loop"sv; - } - - return 0; -} - -/** + /** * @brief Run the system tray with platform specific options. * @note macOS requires that UI elements be created on the main thread, so the system tray is not implemented for macOS. */ -void run_tray() { - // create the system tray -#if defined(__APPLE__) || defined(__MACH__) - // macOS requires that UI elements be created on the main thread - // creating tray using dispatch queue does not work, although the code doesn't actually throw any (visible) errors + void + run_tray() { + // create the system tray + #if defined(__APPLE__) || defined(__MACH__) + // macOS requires that UI elements be created on the main thread + // creating tray using dispatch queue does not work, although the code doesn't actually throw any (visible) errors - // dispatch_async(dispatch_get_main_queue(), ^{ - // system_tray(); - // }); + // dispatch_async(dispatch_get_main_queue(), ^{ + // system_tray(); + // }); - BOOST_LOG(info) << "system_tray() is not yet implemented for this platform."sv; -#else // Windows, Linux - // create tray in separate thread - std::thread tray_thread(system_tray); - tray_thread.detach(); -#endif -} + BOOST_LOG(info) << "system_tray() is not yet implemented for this platform."sv; + #else // Windows, Linux + // create tray in separate thread + std::thread tray_thread(system_tray); + tray_thread.detach(); + #endif + } -/** + /** * @brief Exit the system tray. * @return 0 after exiting the system tray. */ -int end_tray() { - tray_exit(); - return 0; -} + int + end_tray() { + tray_exit(); + return 0; + } -} // namespace system_tray +} // namespace system_tray #endif diff --git a/src/system_tray.h b/src/system_tray.h index a760934e..46849883 100644 --- a/src/system_tray.h +++ b/src/system_tray.h @@ -7,17 +7,27 @@ // system_tray namespace namespace system_tray { -void open_url(const std::string &url); -void tray_open_ui_cb(struct tray_menu *item); -void tray_donate_github_cb(struct tray_menu *item); -void tray_donate_mee6_cb(struct tray_menu *item); -void tray_donate_patreon_cb(struct tray_menu *item); -void tray_donate_paypal_cb(struct tray_menu *item); -void tray_quit_cb(struct tray_menu *item); + void + open_url(const std::string &url); + void + tray_open_ui_cb(struct tray_menu *item); + void + tray_donate_github_cb(struct tray_menu *item); + void + tray_donate_mee6_cb(struct tray_menu *item); + void + tray_donate_patreon_cb(struct tray_menu *item); + void + tray_donate_paypal_cb(struct tray_menu *item); + void + tray_quit_cb(struct tray_menu *item); -int system_tray(); -int run_tray(); -int end_tray(); + int + system_tray(); + int + run_tray(); + int + end_tray(); -} // namespace system_tray +} // namespace system_tray #endif diff --git a/src/task_pool.h b/src/task_pool.h index 69f5f21a..193f8f23 100644 --- a/src/task_pool.h +++ b/src/task_pool.h @@ -15,231 +15,246 @@ #include "utility.h" namespace task_pool_util { -class _ImplBase { -public: - // _unique_base_type _this_ptr; - - inline virtual ~_ImplBase() = default; - - virtual void run() = 0; -}; - -template -class _Impl : public _ImplBase { - Function _func; - -public: - _Impl(Function &&f) : _func(std::forward(f)) {} - - void run() override { - _func(); - } -}; - -class TaskPool { -public: - typedef std::unique_ptr<_ImplBase> __task; - typedef _ImplBase *task_id_t; - - - typedef std::chrono::steady_clock::time_point __time_point; - - template - class timer_task_t { + class _ImplBase { public: - task_id_t task_id; - std::future future; + // _unique_base_type _this_ptr; - timer_task_t(task_id_t task_id, std::future &future) : task_id { task_id }, future { std::move(future) } {} + inline virtual ~_ImplBase() = default; + + virtual void + run() = 0; }; -protected: - std::deque<__task> _tasks; - std::vector> _timer_tasks; - std::mutex _task_mutex; + template + class _Impl: public _ImplBase { + Function _func; -public: - TaskPool() = default; - TaskPool(TaskPool &&other) noexcept : _tasks { std::move(other._tasks) }, _timer_tasks { std::move(other._timer_tasks) } {} + public: + _Impl(Function &&f): + _func(std::forward(f)) {} - TaskPool &operator=(TaskPool &&other) noexcept { - std::swap(_tasks, other._tasks); - std::swap(_timer_tasks, other._timer_tasks); + void + run() override { + _func(); + } + }; - return *this; - } + class TaskPool { + public: + typedef std::unique_ptr<_ImplBase> __task; + typedef _ImplBase *task_id_t; - template - auto push(Function &&newTask, Args &&...args) { - static_assert(std::is_invocable_v, "arguments don't match the function"); + typedef std::chrono::steady_clock::time_point __time_point; - using __return = std::invoke_result_t; - using task_t = std::packaged_task<__return()>; + template + class timer_task_t { + public: + task_id_t task_id; + std::future future; - auto bind = [task = std::forward(newTask), tuple_args = std::make_tuple(std::forward(args)...)]() mutable { - return std::apply(task, std::move(tuple_args)); + timer_task_t(task_id_t task_id, std::future &future): + task_id { task_id }, future { std::move(future) } {} }; - task_t task(std::move(bind)); + protected: + std::deque<__task> _tasks; + std::vector> _timer_tasks; + std::mutex _task_mutex; - auto future = task.get_future(); + public: + TaskPool() = default; + TaskPool(TaskPool &&other) noexcept: + _tasks { std::move(other._tasks) }, _timer_tasks { std::move(other._timer_tasks) } {} - std::lock_guard lg(_task_mutex); - _tasks.emplace_back(toRunnable(std::move(task))); + TaskPool & + operator=(TaskPool &&other) noexcept { + std::swap(_tasks, other._tasks); + std::swap(_timer_tasks, other._timer_tasks); - return future; - } - - void pushDelayed(std::pair<__time_point, __task> &&task) { - std::lock_guard lg(_task_mutex); - - auto it = _timer_tasks.cbegin(); - for(; it < _timer_tasks.cend(); ++it) { - if(std::get<0>(*it) < task.first) { - break; - } + return *this; } - _timer_tasks.emplace(it, task.first, std::move(task.second)); - } + template + auto + push(Function &&newTask, Args &&...args) { + static_assert(std::is_invocable_v, "arguments don't match the function"); - /** + using __return = std::invoke_result_t; + using task_t = std::packaged_task<__return()>; + + auto bind = [task = std::forward(newTask), tuple_args = std::make_tuple(std::forward(args)...)]() mutable { + return std::apply(task, std::move(tuple_args)); + }; + + task_t task(std::move(bind)); + + auto future = task.get_future(); + + std::lock_guard lg(_task_mutex); + _tasks.emplace_back(toRunnable(std::move(task))); + + return future; + } + + void + pushDelayed(std::pair<__time_point, __task> &&task) { + std::lock_guard lg(_task_mutex); + + auto it = _timer_tasks.cbegin(); + for (; it < _timer_tasks.cend(); ++it) { + if (std::get<0>(*it) < task.first) { + break; + } + } + + _timer_tasks.emplace(it, task.first, std::move(task.second)); + } + + /** * @return an id to potentially delay the task */ - template - auto pushDelayed(Function &&newTask, std::chrono::duration duration, Args &&...args) { - static_assert(std::is_invocable_v, "arguments don't match the function"); + template + auto + pushDelayed(Function &&newTask, std::chrono::duration duration, Args &&...args) { + static_assert(std::is_invocable_v, "arguments don't match the function"); - using __return = std::invoke_result_t; - using task_t = std::packaged_task<__return()>; + using __return = std::invoke_result_t; + using task_t = std::packaged_task<__return()>; - __time_point time_point; - if constexpr(std::is_floating_point_v) { - time_point = std::chrono::steady_clock::now() + std::chrono::duration_cast(duration); - } - else { - time_point = std::chrono::steady_clock::now() + duration; + __time_point time_point; + if constexpr (std::is_floating_point_v) { + time_point = std::chrono::steady_clock::now() + std::chrono::duration_cast(duration); + } + else { + time_point = std::chrono::steady_clock::now() + duration; + } + + auto bind = [task = std::forward(newTask), tuple_args = std::make_tuple(std::forward(args)...)]() mutable { + return std::apply(task, std::move(tuple_args)); + }; + + task_t task(std::move(bind)); + + auto future = task.get_future(); + auto runnable = toRunnable(std::move(task)); + + task_id_t task_id = &*runnable; + + pushDelayed(std::pair { time_point, std::move(runnable) }); + + return timer_task_t<__return> { task_id, future }; } - auto bind = [task = std::forward(newTask), tuple_args = std::make_tuple(std::forward(args)...)]() mutable { - return std::apply(task, std::move(tuple_args)); - }; - - task_t task(std::move(bind)); - - auto future = task.get_future(); - auto runnable = toRunnable(std::move(task)); - - task_id_t task_id = &*runnable; - - pushDelayed(std::pair { time_point, std::move(runnable) }); - - return timer_task_t<__return> { task_id, future }; - } - - /** + /** * @param duration The delay before executing the task */ - template - void delay(task_id_t task_id, std::chrono::duration duration) { - std::lock_guard lg(_task_mutex); + template + void + delay(task_id_t task_id, std::chrono::duration duration) { + std::lock_guard lg(_task_mutex); - auto it = _timer_tasks.begin(); - for(; it < _timer_tasks.cend(); ++it) { - const __task &task = std::get<1>(*it); + auto it = _timer_tasks.begin(); + for (; it < _timer_tasks.cend(); ++it) { + const __task &task = std::get<1>(*it); - if(&*task == task_id) { - std::get<0>(*it) = std::chrono::steady_clock::now() + duration; + if (&*task == task_id) { + std::get<0>(*it) = std::chrono::steady_clock::now() + duration; - break; + break; + } + } + + if (it == _timer_tasks.cend()) { + return; + } + + // smaller time goes to the back + auto prev = it - 1; + while (it > _timer_tasks.cbegin()) { + if (std::get<0>(*it) > std::get<0>(*prev)) { + std::swap(*it, *prev); + } + + --prev; + --it; } } - if(it == _timer_tasks.cend()) { - return; - } + bool + cancel(task_id_t task_id) { + std::lock_guard lg(_task_mutex); - // smaller time goes to the back - auto prev = it - 1; - while(it > _timer_tasks.cbegin()) { - if(std::get<0>(*it) > std::get<0>(*prev)) { - std::swap(*it, *prev); + auto it = _timer_tasks.begin(); + for (; it < _timer_tasks.cend(); ++it) { + const __task &task = std::get<1>(*it); + + if (&*task == task_id) { + _timer_tasks.erase(it); + + return true; + } } - --prev; - --it; + return false; } - } - bool cancel(task_id_t task_id) { - std::lock_guard lg(_task_mutex); + std::optional> + pop(task_id_t task_id) { + std::lock_guard lg(_task_mutex); - auto it = _timer_tasks.begin(); - for(; it < _timer_tasks.cend(); ++it) { - const __task &task = std::get<1>(*it); + auto pos = std::find_if(std::begin(_timer_tasks), std::end(_timer_tasks), [&task_id](const auto &t) { return t.second.get() == task_id; }); - if(&*task == task_id) { - _timer_tasks.erase(it); - - return true; + if (pos == std::end(_timer_tasks)) { + return std::nullopt; } + + return std::move(*pos); } - return false; - } + std::optional<__task> + pop() { + std::lock_guard lg(_task_mutex); - std::optional> pop(task_id_t task_id) { - std::lock_guard lg(_task_mutex); + if (!_tasks.empty()) { + __task task = std::move(_tasks.front()); + _tasks.pop_front(); + return std::move(task); + } - auto pos = std::find_if(std::begin(_timer_tasks), std::end(_timer_tasks), [&task_id](const auto &t) { return t.second.get() == task_id; }); + if (!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now()) { + __task task = std::move(std::get<1>(_timer_tasks.back())); + _timer_tasks.pop_back(); + + return std::move(task); + } - if(pos == std::end(_timer_tasks)) { return std::nullopt; } - return std::move(*pos); - } + bool + ready() { + std::lock_guard lg(_task_mutex); - std::optional<__task> pop() { - std::lock_guard lg(_task_mutex); - - if(!_tasks.empty()) { - __task task = std::move(_tasks.front()); - _tasks.pop_front(); - return std::move(task); + return !_tasks.empty() || (!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now()); } - if(!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now()) { - __task task = std::move(std::get<1>(_timer_tasks.back())); - _timer_tasks.pop_back(); + std::optional<__time_point> + next() { + std::lock_guard lg(_task_mutex); - return std::move(task); + if (_timer_tasks.empty()) { + return std::nullopt; + } + + return std::get<0>(_timer_tasks.back()); } - return std::nullopt; - } - - bool ready() { - std::lock_guard lg(_task_mutex); - - return !_tasks.empty() || (!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now()); - } - - std::optional<__time_point> next() { - std::lock_guard lg(_task_mutex); - - if(_timer_tasks.empty()) { - return std::nullopt; + private: + template + std::unique_ptr<_ImplBase> + toRunnable(Function &&f) { + return std::make_unique<_Impl>(std::forward(f)); } - - return std::get<0>(_timer_tasks.back()); - } - -private: - template - std::unique_ptr<_ImplBase> toRunnable(Function &&f) { - return std::make_unique<_Impl>(std::forward(f)); - } -}; -} // namespace task_pool_util + }; +} // namespace task_pool_util #endif diff --git a/src/thread_pool.h b/src/thread_pool.h index ec6f535c..5de54758 100644 --- a/src/thread_pool.h +++ b/src/thread_pool.h @@ -5,117 +5,126 @@ #include namespace thread_pool_util { -/* + /* * Allow threads to execute unhindered * while keeping full control over the threads. */ -class ThreadPool : public task_pool_util::TaskPool { -public: - typedef TaskPool::__task __task; + class ThreadPool: public task_pool_util::TaskPool { + public: + typedef TaskPool::__task __task; -private: - std::vector _thread; + private: + std::vector _thread; - std::condition_variable _cv; - std::mutex _lock; + std::condition_variable _cv; + std::mutex _lock; - bool _continue; + bool _continue; -public: - ThreadPool() : _continue { false } {} + public: + ThreadPool(): + _continue { false } {} - explicit ThreadPool(int threads) : _thread(threads), _continue { true } { - for(auto &t : _thread) { - t = std::thread(&ThreadPool::_main, this); - } - } - - ~ThreadPool() noexcept { - if(!_continue) return; - - stop(); - join(); - } - - template - auto push(Function &&newTask, Args &&...args) { - std::lock_guard lg(_lock); - auto future = TaskPool::push(std::forward(newTask), std::forward(args)...); - - _cv.notify_one(); - return future; - } - - void pushDelayed(std::pair<__time_point, __task> &&task) { - std::lock_guard lg(_lock); - - TaskPool::pushDelayed(std::move(task)); - } - - template - auto pushDelayed(Function &&newTask, std::chrono::duration duration, Args &&...args) { - std::lock_guard lg(_lock); - auto future = TaskPool::pushDelayed(std::forward(newTask), duration, std::forward(args)...); - - // Update all timers for wait_until - _cv.notify_all(); - return future; - } - - void start(int threads) { - _continue = true; - - _thread.resize(threads); - - for(auto &t : _thread) { - t = std::thread(&ThreadPool::_main, this); - } - } - - void stop() { - std::lock_guard lg(_lock); - - _continue = false; - _cv.notify_all(); - } - - void join() { - for(auto &t : _thread) { - t.join(); - } - } - -public: - void _main() { - while(_continue) { - if(auto task = this->pop()) { - (*task)->run(); + explicit ThreadPool(int threads): + _thread(threads), _continue { true } { + for (auto &t : _thread) { + t = std::thread(&ThreadPool::_main, this); } - else { - std::unique_lock uniq_lock(_lock); + } - if(ready()) { - continue; - } + ~ThreadPool() noexcept { + if (!_continue) return; - if(!_continue) { - break; - } + stop(); + join(); + } - if(auto tp = next()) { - _cv.wait_until(uniq_lock, *tp); + template + auto + push(Function &&newTask, Args &&...args) { + std::lock_guard lg(_lock); + auto future = TaskPool::push(std::forward(newTask), std::forward(args)...); + + _cv.notify_one(); + return future; + } + + void + pushDelayed(std::pair<__time_point, __task> &&task) { + std::lock_guard lg(_lock); + + TaskPool::pushDelayed(std::move(task)); + } + + template + auto + pushDelayed(Function &&newTask, std::chrono::duration duration, Args &&...args) { + std::lock_guard lg(_lock); + auto future = TaskPool::pushDelayed(std::forward(newTask), duration, std::forward(args)...); + + // Update all timers for wait_until + _cv.notify_all(); + return future; + } + + void + start(int threads) { + _continue = true; + + _thread.resize(threads); + + for (auto &t : _thread) { + t = std::thread(&ThreadPool::_main, this); + } + } + + void + stop() { + std::lock_guard lg(_lock); + + _continue = false; + _cv.notify_all(); + } + + void + join() { + for (auto &t : _thread) { + t.join(); + } + } + + public: + void + _main() { + while (_continue) { + if (auto task = this->pop()) { + (*task)->run(); } else { - _cv.wait(uniq_lock); + std::unique_lock uniq_lock(_lock); + + if (ready()) { + continue; + } + + if (!_continue) { + break; + } + + if (auto tp = next()) { + _cv.wait_until(uniq_lock, *tp); + } + else { + _cv.wait(uniq_lock); + } } } - } - // Execute remaining tasks - while(auto task = this->pop()) { - (*task)->run(); + // Execute remaining tasks + while (auto task = this->pop()) { + (*task)->run(); + } } - } -}; -} // namespace thread_pool_util + }; +} // namespace thread_pool_util #endif diff --git a/src/thread_safe.h b/src/thread_safe.h index b14b3234..1f29761f 100644 --- a/src/thread_safe.h +++ b/src/thread_safe.h @@ -13,497 +13,545 @@ #include "utility.h" namespace safe { -template -class event_t { -public: - using status_t = util::optional_t; - - template - void raise(Args &&...args) { - std::lock_guard lg { _lock }; - if(!_continue) { - return; - } - - if constexpr(std::is_same_v, status_t>) { - _status = std::make_optional(std::forward(args)...); - } - else { - _status = status_t { std::forward(args)... }; - } - - _cv.notify_all(); - } - - // pop and view shoud not be used interchangeably - status_t pop() { - std::unique_lock ul { _lock }; - - if(!_continue) { - return util::false_v; - } - - while(!_status) { - _cv.wait(ul); - - if(!_continue) { - return util::false_v; - } - } - - auto val = std::move(_status); - _status = util::false_v; - return val; - } - - // pop and view shoud not be used interchangeably - template - status_t pop(std::chrono::duration delay) { - std::unique_lock ul { _lock }; - - if(!_continue) { - return util::false_v; - } - - while(!_status) { - if(!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) { - return util::false_v; - } - } - - auto val = std::move(_status); - _status = util::false_v; - return val; - } - - // pop and view shoud not be used interchangeably - const status_t &view() { - std::unique_lock ul { _lock }; - - if(!_continue) { - return util::false_v; - } - - while(!_status) { - _cv.wait(ul); - - if(!_continue) { - return util::false_v; - } - } - - return _status; - } - - bool peek() { - return _continue && (bool)_status; - } - - void stop() { - std::lock_guard lg { _lock }; - - _continue = false; - - _cv.notify_all(); - } - - void reset() { - std::lock_guard lg { _lock }; - - _continue = true; - - _status = util::false_v; - } - - [[nodiscard]] bool running() const { - return _continue; - } - -private: - bool _continue { true }; - status_t _status { util::false_v }; - - std::condition_variable _cv; - std::mutex _lock; -}; - -template -class alarm_raw_t { -public: - using status_t = util::optional_t; - - alarm_raw_t() : _status { util::false_v } {} - - void ring(const status_t &status) { - std::lock_guard lg(_lock); - - _status = status; - _cv.notify_one(); - } - - void ring(status_t &&status) { - std::lock_guard lg(_lock); - - _status = std::move(status); - _cv.notify_one(); - } - - template - auto wait_for(const std::chrono::duration &rel_time) { - std::unique_lock ul(_lock); - - return _cv.wait_for(ul, rel_time, [this]() { return (bool)status(); }); - } - - template - auto wait_for(const std::chrono::duration &rel_time, Pred &&pred) { - std::unique_lock ul(_lock); - - return _cv.wait_for(ul, rel_time, [this, &pred]() { return (bool)status() || pred(); }); - } - - template - auto wait_until(const std::chrono::duration &rel_time) { - std::unique_lock ul(_lock); - - return _cv.wait_until(ul, rel_time, [this]() { return (bool)status(); }); - } - - template - auto wait_until(const std::chrono::duration &rel_time, Pred &&pred) { - std::unique_lock ul(_lock); - - return _cv.wait_until(ul, rel_time, [this, &pred]() { return (bool)status() || pred(); }); - } - - auto wait() { - std::unique_lock ul(_lock); - _cv.wait(ul, [this]() { return (bool)status(); }); - } - - template - auto wait(Pred &&pred) { - std::unique_lock ul(_lock); - _cv.wait(ul, [this, &pred]() { return (bool)status() || pred(); }); - } - - const status_t &status() const { - return _status; - } - - status_t &status() { - return _status; - } - - void reset() { - _status = status_t {}; - } - -private: - std::mutex _lock; - std::condition_variable _cv; - - status_t _status; -}; - -template -using alarm_t = std::shared_ptr>; - -template -alarm_t make_alarm() { - return std::make_shared>(); -} - -template -class queue_t { -public: - using status_t = util::optional_t; - - queue_t(std::uint32_t max_elements = 32) : _max_elements { max_elements } {} - - template - void raise(Args &&...args) { - std::lock_guard ul { _lock }; - - if(!_continue) { - return; - } - - if(_queue.size() == _max_elements) { - _queue.clear(); - } - - _queue.emplace_back(std::forward(args)...); - - _cv.notify_all(); - } - - bool peek() { - return _continue && !_queue.empty(); - } - - template - status_t pop(std::chrono::duration delay) { - std::unique_lock ul { _lock }; - - if(!_continue) { - return util::false_v; - } - - while(_queue.empty()) { - if(!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) { - return util::false_v; - } - } - - auto val = std::move(_queue.front()); - _queue.erase(std::begin(_queue)); - - return val; - } - - status_t pop() { - std::unique_lock ul { _lock }; - - if(!_continue) { - return util::false_v; - } - - while(_queue.empty()) { - _cv.wait(ul); - - if(!_continue) { - return util::false_v; - } - } - - auto val = std::move(_queue.front()); - _queue.erase(std::begin(_queue)); - - return val; - } - - std::vector &unsafe() { - return _queue; - } - - void stop() { - std::lock_guard lg { _lock }; - - _continue = false; - - _cv.notify_all(); - } - - [[nodiscard]] bool running() const { - return _continue; - } - -private: - bool _continue { true }; - std::uint32_t _max_elements; - - std::mutex _lock; - std::condition_variable _cv; - - std::vector _queue; -}; - -template -class shared_t { -public: - using element_type = T; - - using construct_f = std::function; - using destruct_f = std::function; - - struct ptr_t { - shared_t *owner; - - ptr_t() : owner { nullptr } {} - explicit ptr_t(shared_t *owner) : owner { owner } {} - - ptr_t(ptr_t &&ptr) noexcept : owner { ptr.owner } { - ptr.owner = nullptr; - } - - ptr_t(const ptr_t &ptr) noexcept : owner { ptr.owner } { - if(!owner) { + template + class event_t { + public: + using status_t = util::optional_t; + + template + void + raise(Args &&...args) { + std::lock_guard lg { _lock }; + if (!_continue) { return; } - auto tmp = ptr.owner->ref(); - tmp.owner = nullptr; + if constexpr (std::is_same_v, status_t>) { + _status = std::make_optional(std::forward(args)...); + } + else { + _status = status_t { std::forward(args)... }; + } + + _cv.notify_all(); } - ptr_t &operator=(const ptr_t &ptr) noexcept { - if(!ptr.owner) { - release(); + // pop and view shoud not be used interchangeably + status_t + pop() { + std::unique_lock ul { _lock }; + + if (!_continue) { + return util::false_v; + } + + while (!_status) { + _cv.wait(ul); + + if (!_continue) { + return util::false_v; + } + } + + auto val = std::move(_status); + _status = util::false_v; + return val; + } + + // pop and view shoud not be used interchangeably + template + status_t + pop(std::chrono::duration delay) { + std::unique_lock ul { _lock }; + + if (!_continue) { + return util::false_v; + } + + while (!_status) { + if (!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) { + return util::false_v; + } + } + + auto val = std::move(_status); + _status = util::false_v; + return val; + } + + // pop and view shoud not be used interchangeably + const status_t & + view() { + std::unique_lock ul { _lock }; + + if (!_continue) { + return util::false_v; + } + + while (!_status) { + _cv.wait(ul); + + if (!_continue) { + return util::false_v; + } + } + + return _status; + } + + bool + peek() { + return _continue && (bool) _status; + } + + void + stop() { + std::lock_guard lg { _lock }; + + _continue = false; + + _cv.notify_all(); + } + + void + reset() { + std::lock_guard lg { _lock }; + + _continue = true; + + _status = util::false_v; + } + + [[nodiscard]] bool + running() const { + return _continue; + } + + private: + bool _continue { true }; + status_t _status { util::false_v }; + + std::condition_variable _cv; + std::mutex _lock; + }; + + template + class alarm_raw_t { + public: + using status_t = util::optional_t; + + alarm_raw_t(): + _status { util::false_v } {} + + void + ring(const status_t &status) { + std::lock_guard lg(_lock); + + _status = status; + _cv.notify_one(); + } + + void + ring(status_t &&status) { + std::lock_guard lg(_lock); + + _status = std::move(status); + _cv.notify_one(); + } + + template + auto + wait_for(const std::chrono::duration &rel_time) { + std::unique_lock ul(_lock); + + return _cv.wait_for(ul, rel_time, [this]() { return (bool) status(); }); + } + + template + auto + wait_for(const std::chrono::duration &rel_time, Pred &&pred) { + std::unique_lock ul(_lock); + + return _cv.wait_for(ul, rel_time, [this, &pred]() { return (bool) status() || pred(); }); + } + + template + auto + wait_until(const std::chrono::duration &rel_time) { + std::unique_lock ul(_lock); + + return _cv.wait_until(ul, rel_time, [this]() { return (bool) status(); }); + } + + template + auto + wait_until(const std::chrono::duration &rel_time, Pred &&pred) { + std::unique_lock ul(_lock); + + return _cv.wait_until(ul, rel_time, [this, &pred]() { return (bool) status() || pred(); }); + } + + auto + wait() { + std::unique_lock ul(_lock); + _cv.wait(ul, [this]() { return (bool) status(); }); + } + + template + auto + wait(Pred &&pred) { + std::unique_lock ul(_lock); + _cv.wait(ul, [this, &pred]() { return (bool) status() || pred(); }); + } + + const status_t & + status() const { + return _status; + } + + status_t & + status() { + return _status; + } + + void + reset() { + _status = status_t {}; + } + + private: + std::mutex _lock; + std::condition_variable _cv; + + status_t _status; + }; + + template + using alarm_t = std::shared_ptr>; + + template + alarm_t + make_alarm() { + return std::make_shared>(); + } + + template + class queue_t { + public: + using status_t = util::optional_t; + + queue_t(std::uint32_t max_elements = 32): + _max_elements { max_elements } {} + + template + void + raise(Args &&...args) { + std::lock_guard ul { _lock }; + + if (!_continue) { + return; + } + + if (_queue.size() == _max_elements) { + _queue.clear(); + } + + _queue.emplace_back(std::forward(args)...); + + _cv.notify_all(); + } + + bool + peek() { + return _continue && !_queue.empty(); + } + + template + status_t + pop(std::chrono::duration delay) { + std::unique_lock ul { _lock }; + + if (!_continue) { + return util::false_v; + } + + while (_queue.empty()) { + if (!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) { + return util::false_v; + } + } + + auto val = std::move(_queue.front()); + _queue.erase(std::begin(_queue)); + + return val; + } + + status_t + pop() { + std::unique_lock ul { _lock }; + + if (!_continue) { + return util::false_v; + } + + while (_queue.empty()) { + _cv.wait(ul); + + if (!_continue) { + return util::false_v; + } + } + + auto val = std::move(_queue.front()); + _queue.erase(std::begin(_queue)); + + return val; + } + + std::vector & + unsafe() { + return _queue; + } + + void + stop() { + std::lock_guard lg { _lock }; + + _continue = false; + + _cv.notify_all(); + } + + [[nodiscard]] bool + running() const { + return _continue; + } + + private: + bool _continue { true }; + std::uint32_t _max_elements; + + std::mutex _lock; + std::condition_variable _cv; + + std::vector _queue; + }; + + template + class shared_t { + public: + using element_type = T; + + using construct_f = std::function; + using destruct_f = std::function; + + struct ptr_t { + shared_t *owner; + + ptr_t(): + owner { nullptr } {} + explicit ptr_t(shared_t *owner): + owner { owner } {} + + ptr_t(ptr_t &&ptr) noexcept: + owner { ptr.owner } { + ptr.owner = nullptr; + } + + ptr_t(const ptr_t &ptr) noexcept: + owner { ptr.owner } { + if (!owner) { + return; + } + + auto tmp = ptr.owner->ref(); + tmp.owner = nullptr; + } + + ptr_t & + operator=(const ptr_t &ptr) noexcept { + if (!ptr.owner) { + release(); + + return *this; + } + + return *this = std::move(*ptr.owner->ref()); + } + + ptr_t & + operator=(ptr_t &&ptr) noexcept { + if (owner) { + release(); + } + + std::swap(owner, ptr.owner); return *this; } - return *this = std::move(*ptr.owner->ref()); - } - - ptr_t &operator=(ptr_t &&ptr) noexcept { - if(owner) { - release(); + ~ptr_t() { + if (owner) { + release(); + } } - std::swap(owner, ptr.owner); - - return *this; - } - - ~ptr_t() { - if(owner) { - release(); - } - } - - operator bool() const { - return owner != nullptr; - } - - void release() { - std::lock_guard lg { owner->_lock }; - - if(!--owner->_count) { - owner->_destruct(*get()); - (*this)->~element_type(); + operator bool() const { + return owner != nullptr; } - owner = nullptr; + void + release() { + std::lock_guard lg { owner->_lock }; + + if (!--owner->_count) { + owner->_destruct(*get()); + (*this)->~element_type(); + } + + owner = nullptr; + } + + element_type * + get() const { + return reinterpret_cast(owner->_object_buf.data()); + } + + element_type * + operator->() { + return reinterpret_cast(owner->_object_buf.data()); + } + }; + + template + shared_t(FC &&fc, FD &&fd): + _construct { std::forward(fc) }, _destruct { std::forward(fd) } {} + [[nodiscard]] ptr_t + ref() { + std::lock_guard lg { _lock }; + + if (!_count) { + new (_object_buf.data()) element_type; + if (_construct(*reinterpret_cast(_object_buf.data()))) { + return ptr_t { nullptr }; + } + } + + ++_count; + + return ptr_t { this }; } - element_type *get() const { - return reinterpret_cast(owner->_object_buf.data()); - } + private: + construct_f _construct; + destruct_f _destruct; - element_type *operator->() { - return reinterpret_cast(owner->_object_buf.data()); + std::array _object_buf; + + std::uint32_t _count; + std::mutex _lock; + }; + + template + auto + make_shared(F_Construct &&fc, F_Destruct &&fd) { + return shared_t { + std::forward(fc), std::forward(fd) + }; + } + + using signal_t = event_t; + + class mail_raw_t; + using mail_t = std::shared_ptr; + + void + cleanup(mail_raw_t *); + template + class post_t: public T { + public: + template + post_t(mail_t mail, Args &&...args): + T(std::forward(args)...), mail { std::move(mail) } {} + + mail_t mail; + + ~post_t() { + cleanup(mail.get()); } }; - template - shared_t(FC &&fc, FD &&fd) : _construct { std::forward(fc) }, _destruct { std::forward(fd) } {} - [[nodiscard]] ptr_t ref() { - std::lock_guard lg { _lock }; + template + inline auto + lock(const std::weak_ptr &wp) { + return std::reinterpret_pointer_cast(wp.lock()); + } - if(!_count) { - new(_object_buf.data()) element_type; - if(_construct(*reinterpret_cast(_object_buf.data()))) { - return ptr_t { nullptr }; + class mail_raw_t: public std::enable_shared_from_this { + public: + template + using event_t = std::shared_ptr>>; + + template + using queue_t = std::shared_ptr>>; + + template + event_t + event(const std::string_view &id) { + std::lock_guard lg { mutex }; + + auto it = id_to_post.find(id); + if (it != std::end(id_to_post)) { + return lock>(it->second); + } + + auto post = std::make_shared::element_type>(shared_from_this()); + id_to_post.emplace(std::pair> { std::string { id }, post }); + + return post; + } + + template + queue_t + queue(const std::string_view &id) { + std::lock_guard lg { mutex }; + + auto it = id_to_post.find(id); + if (it != std::end(id_to_post)) { + return lock>(it->second); + } + + auto post = std::make_shared::element_type>(shared_from_this(), 32); + id_to_post.emplace(std::pair> { std::string { id }, post }); + + return post; + } + + void + cleanup() { + std::lock_guard lg { mutex }; + + for (auto it = std::begin(id_to_post); it != std::end(id_to_post); ++it) { + auto &weak = it->second; + + if (weak.expired()) { + id_to_post.erase(it); + + return; + } } } - ++_count; + std::mutex mutex; - return ptr_t { this }; - } - -private: - construct_f _construct; - destruct_f _destruct; - - std::array _object_buf; - - std::uint32_t _count; - std::mutex _lock; -}; - -template -auto make_shared(F_Construct &&fc, F_Destruct &&fd) { - return shared_t { - std::forward(fc), std::forward(fd) + std::map, std::less<>> id_to_post; }; -} -using signal_t = event_t; - -class mail_raw_t; -using mail_t = std::shared_ptr; - -void cleanup(mail_raw_t *); -template -class post_t : public T { -public: - template - post_t(mail_t mail, Args &&...args) : T(std::forward(args)...), mail { std::move(mail) } {} - - mail_t mail; - - ~post_t() { - cleanup(mail.get()); + inline void + cleanup(mail_raw_t *mail) { + mail->cleanup(); } -}; +} // namespace safe -template -inline auto lock(const std::weak_ptr &wp) { - return std::reinterpret_pointer_cast(wp.lock()); -} - -class mail_raw_t : public std::enable_shared_from_this { -public: - template - using event_t = std::shared_ptr>>; - - template - using queue_t = std::shared_ptr>>; - - template - event_t event(const std::string_view &id) { - std::lock_guard lg { mutex }; - - auto it = id_to_post.find(id); - if(it != std::end(id_to_post)) { - return lock>(it->second); - } - - auto post = std::make_shared::element_type>(shared_from_this()); - id_to_post.emplace(std::pair> { std::string { id }, post }); - - return post; - } - - template - queue_t queue(const std::string_view &id) { - std::lock_guard lg { mutex }; - - auto it = id_to_post.find(id); - if(it != std::end(id_to_post)) { - return lock>(it->second); - } - - auto post = std::make_shared::element_type>(shared_from_this(), 32); - id_to_post.emplace(std::pair> { std::string { id }, post }); - - return post; - } - - void cleanup() { - std::lock_guard lg { mutex }; - - for(auto it = std::begin(id_to_post); it != std::end(id_to_post); ++it) { - auto &weak = it->second; - - if(weak.expired()) { - id_to_post.erase(it); - - return; - } - } - } - - std::mutex mutex; - - std::map, std::less<>> id_to_post; -}; - -inline void cleanup(mail_raw_t *mail) { - mail->cleanup(); -} -} // namespace safe - -#endif // SUNSHINE_THREAD_SAFE_H +#endif // SUNSHINE_THREAD_SAFE_H diff --git a/src/upnp.cpp b/src/upnp.cpp index 4bdfee96..7a0a9213 100644 --- a/src/upnp.cpp +++ b/src/upnp.cpp @@ -14,173 +14,175 @@ using namespace std::literals; namespace upnp { -constexpr auto INET6_ADDRESS_STRLEN = 46; + constexpr auto INET6_ADDRESS_STRLEN = 46; -constexpr auto IPv4 = 0; -constexpr auto IPv6 = 1; + constexpr auto IPv4 = 0; + constexpr auto IPv6 = 1; -using device_t = util::safe_ptr; + using device_t = util::safe_ptr; -KITTY_USING_MOVE_T(urls_t, UPNPUrls, , { - FreeUPNPUrls(&el); -}); + KITTY_USING_MOVE_T(urls_t, UPNPUrls, , { + FreeUPNPUrls(&el); + }); -struct mapping_t { - struct { - std::string wan; - std::string lan; - } port; + struct mapping_t { + struct { + std::string wan; + std::string lan; + } port; - std::string description; - bool tcp; -}; - -void unmap( - const urls_t &urls, - const IGDdatas &data, - std::vector::const_reverse_iterator begin, - std::vector::const_reverse_iterator end) { - - BOOST_LOG(debug) << "Unmapping UPNP ports"sv; - - for(auto it = begin; it != end; ++it) { - auto status = UPNP_DeletePortMapping( - urls->controlURL, - data.first.servicetype, - it->port.wan.c_str(), - it->tcp ? "TCP" : "UDP", - nullptr); - - if(status) { - BOOST_LOG(warning) << "Failed to unmap port ["sv << it->port.wan << "] to port ["sv << it->port.lan << "]: error code ["sv << status << ']'; - break; - } - } -} - -class deinit_t : public platf::deinit_t { -public: - using iter_t = std::vector::const_reverse_iterator; - deinit_t(urls_t &&urls, IGDdatas data, std::vector &&mapping) - : urls { std::move(urls) }, data { data }, mapping { std::move(mapping) } {} - - ~deinit_t() { - BOOST_LOG(info) << "Unmapping UPNP ports..."sv; - unmap(urls, data, std::rbegin(mapping), std::rend(mapping)); - } - - urls_t urls; - IGDdatas data; - - std::vector mapping; -}; - -static std::string_view status_string(int status) { - switch(status) { - case 0: - return "No IGD device found"sv; - case 1: - return "Valid IGD device found"sv; - case 2: - return "Valid IGD device found, but it isn't connected"sv; - case 3: - return "A UPnP device has been found, but it wasn't recognized as an IGD"sv; - } - - return "Unknown status"sv; -} - -std::unique_ptr start() { - if(!config::sunshine.flags[config::flag::UPNP]) { - return nullptr; - } - - int err {}; - - device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err) }; - if(!device || err) { - BOOST_LOG(error) << "Couldn't discover any UPNP devices"sv; - - return nullptr; - } - - for(auto dev = device.get(); dev != nullptr; dev = dev->pNext) { - BOOST_LOG(debug) << "Found device: "sv << dev->descURL; - } - - std::array lan_addr; - std::array wan_addr; - - urls_t urls; - IGDdatas data; - - auto status = UPNP_GetValidIGD(device.get(), &urls.el, &data, lan_addr.data(), lan_addr.size()); - if(status != 1 && status != 2) { - BOOST_LOG(error) << status_string(status); - return nullptr; - } - - BOOST_LOG(debug) << "Found valid IGD device: "sv << urls->rootdescURL; - - if(UPNP_GetExternalIPAddress(urls->controlURL, data.first.servicetype, wan_addr.data())) { - BOOST_LOG(warning) << "Could not get external ip"sv; - } - else { - BOOST_LOG(debug) << "Found external ip: "sv << wan_addr.data(); - if(config::nvhttp.external_ip.empty()) { - config::nvhttp.external_ip = wan_addr.data(); - } - } - - auto rtsp = std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT)); - auto video = std::to_string(map_port(stream::VIDEO_STREAM_PORT)); - auto audio = std::to_string(map_port(stream::AUDIO_STREAM_PORT)); - auto control = std::to_string(map_port(stream::CONTROL_PORT)); - auto gs_http = std::to_string(map_port(nvhttp::PORT_HTTP)); - auto gs_https = std::to_string(map_port(nvhttp::PORT_HTTPS)); - auto wm_http = std::to_string(map_port(confighttp::PORT_HTTPS)); - - std::vector mappings { - { rtsp, rtsp, "RTSP setup port"s, true }, - { video, video, "Video stream port"s, false }, - { audio, audio, "Control stream port"s, false }, - { control, control, "Audio stream port"s, false }, - { gs_http, gs_http, "Gamestream http port"s, true }, - { gs_https, gs_https, "Gamestream https port"s, true }, + std::string description; + bool tcp; }; - // Only map port for the Web Manager if it is configured to accept connection from WAN - if(net::from_enum_string(config::nvhttp.origin_web_ui_allowed) > net::LAN) { - mappings.emplace_back(mapping_t { wm_http, wm_http, "Sunshine Web UI port"s, true }); - } + void + unmap( + const urls_t &urls, + const IGDdatas &data, + std::vector::const_reverse_iterator begin, + std::vector::const_reverse_iterator end) { + BOOST_LOG(debug) << "Unmapping UPNP ports"sv; - auto it = std::begin(mappings); + for (auto it = begin; it != end; ++it) { + auto status = UPNP_DeletePortMapping( + urls->controlURL, + data.first.servicetype, + it->port.wan.c_str(), + it->tcp ? "TCP" : "UDP", + nullptr); - status = 0; - for(; it != std::end(mappings); ++it) { - status = UPNP_AddPortMapping( - urls->controlURL, - data.first.servicetype, - it->port.wan.c_str(), - it->port.lan.c_str(), - lan_addr.data(), - it->description.c_str(), - it->tcp ? "TCP" : "UDP", - nullptr, - "86400"); - - if(status) { - BOOST_LOG(error) << "Failed to map port ["sv << it->port.wan << "] to port ["sv << it->port.lan << "]: error code ["sv << status << ']'; - break; + if (status) { + BOOST_LOG(warning) << "Failed to unmap port ["sv << it->port.wan << "] to port ["sv << it->port.lan << "]: error code ["sv << status << ']'; + break; + } } } - if(status) { - unmap(urls, data, std::make_reverse_iterator(it), std::rend(mappings)); + class deinit_t: public platf::deinit_t { + public: + using iter_t = std::vector::const_reverse_iterator; + deinit_t(urls_t &&urls, IGDdatas data, std::vector &&mapping): + urls { std::move(urls) }, data { data }, mapping { std::move(mapping) } {} - return nullptr; + ~deinit_t() { + BOOST_LOG(info) << "Unmapping UPNP ports..."sv; + unmap(urls, data, std::rbegin(mapping), std::rend(mapping)); + } + + urls_t urls; + IGDdatas data; + + std::vector mapping; + }; + + static std::string_view + status_string(int status) { + switch (status) { + case 0: + return "No IGD device found"sv; + case 1: + return "Valid IGD device found"sv; + case 2: + return "Valid IGD device found, but it isn't connected"sv; + case 3: + return "A UPnP device has been found, but it wasn't recognized as an IGD"sv; + } + + return "Unknown status"sv; } - return std::make_unique(std::move(urls), data, std::move(mappings)); -} -} // namespace upnp + std::unique_ptr + start() { + if (!config::sunshine.flags[config::flag::UPNP]) { + return nullptr; + } + + int err {}; + + device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err) }; + if (!device || err) { + BOOST_LOG(error) << "Couldn't discover any UPNP devices"sv; + + return nullptr; + } + + for (auto dev = device.get(); dev != nullptr; dev = dev->pNext) { + BOOST_LOG(debug) << "Found device: "sv << dev->descURL; + } + + std::array lan_addr; + std::array wan_addr; + + urls_t urls; + IGDdatas data; + + auto status = UPNP_GetValidIGD(device.get(), &urls.el, &data, lan_addr.data(), lan_addr.size()); + if (status != 1 && status != 2) { + BOOST_LOG(error) << status_string(status); + return nullptr; + } + + BOOST_LOG(debug) << "Found valid IGD device: "sv << urls->rootdescURL; + + if (UPNP_GetExternalIPAddress(urls->controlURL, data.first.servicetype, wan_addr.data())) { + BOOST_LOG(warning) << "Could not get external ip"sv; + } + else { + BOOST_LOG(debug) << "Found external ip: "sv << wan_addr.data(); + if (config::nvhttp.external_ip.empty()) { + config::nvhttp.external_ip = wan_addr.data(); + } + } + + auto rtsp = std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT)); + auto video = std::to_string(map_port(stream::VIDEO_STREAM_PORT)); + auto audio = std::to_string(map_port(stream::AUDIO_STREAM_PORT)); + auto control = std::to_string(map_port(stream::CONTROL_PORT)); + auto gs_http = std::to_string(map_port(nvhttp::PORT_HTTP)); + auto gs_https = std::to_string(map_port(nvhttp::PORT_HTTPS)); + auto wm_http = std::to_string(map_port(confighttp::PORT_HTTPS)); + + std::vector mappings { + { rtsp, rtsp, "RTSP setup port"s, true }, + { video, video, "Video stream port"s, false }, + { audio, audio, "Control stream port"s, false }, + { control, control, "Audio stream port"s, false }, + { gs_http, gs_http, "Gamestream http port"s, true }, + { gs_https, gs_https, "Gamestream https port"s, true }, + }; + + // Only map port for the Web Manager if it is configured to accept connection from WAN + if (net::from_enum_string(config::nvhttp.origin_web_ui_allowed) > net::LAN) { + mappings.emplace_back(mapping_t { wm_http, wm_http, "Sunshine Web UI port"s, true }); + } + + auto it = std::begin(mappings); + + status = 0; + for (; it != std::end(mappings); ++it) { + status = UPNP_AddPortMapping( + urls->controlURL, + data.first.servicetype, + it->port.wan.c_str(), + it->port.lan.c_str(), + lan_addr.data(), + it->description.c_str(), + it->tcp ? "TCP" : "UDP", + nullptr, + "86400"); + + if (status) { + BOOST_LOG(error) << "Failed to map port ["sv << it->port.wan << "] to port ["sv << it->port.lan << "]: error code ["sv << status << ']'; + break; + } + } + + if (status) { + unmap(urls, data, std::make_reverse_iterator(it), std::rend(mappings)); + + return nullptr; + } + + return std::make_unique(std::move(urls), data, std::move(mappings)); + } +} // namespace upnp diff --git a/src/upnp.h b/src/upnp.h index 2ab57df6..43068dca 100644 --- a/src/upnp.h +++ b/src/upnp.h @@ -4,7 +4,8 @@ #include "platform/common.h" namespace upnp { -[[nodiscard]] std::unique_ptr start(); + [[nodiscard]] std::unique_ptr + start(); } #endif diff --git a/src/utility.h b/src/utility.h index e40228ac..bb7f87a0 100644 --- a/src/utility.h +++ b/src/utility.h @@ -15,841 +15,937 @@ #define KITTY_WHILE_LOOP(x, y, z) \ { \ x; \ - while(y) z \ + while (y) z \ } -template +template struct argument_type; -template +template struct argument_type { typedef U type; }; -#define KITTY_USING_MOVE_T(move_t, t, init_val, z) \ - class move_t { \ - public: \ - using element_type = typename argument_type::type; \ - \ - move_t() : el { init_val } {} \ - template \ - move_t(Args &&...args) : el { std::forward(args)... } {} \ - move_t(const move_t &) = delete; \ - \ - move_t(move_t &&other) noexcept : el { std::move(other.el) } { \ - other.el = element_type { init_val }; \ - } \ - \ - move_t &operator=(const move_t &) = delete; \ - \ - move_t &operator=(move_t &&other) { \ - std::swap(el, other.el); \ - return *this; \ - } \ - element_type *operator->() { return ⪙ } \ - const element_type *operator->() const { return ⪙ } \ - \ - inline element_type release() { \ - element_type val = std::move(el); \ - el = element_type { init_val }; \ - return val; \ - } \ - \ - ~move_t() z \ - \ - element_type el; \ +#define KITTY_USING_MOVE_T(move_t, t, init_val, z) \ + class move_t { \ + public: \ + using element_type = typename argument_type::type; \ + \ + move_t(): el { init_val } {} \ + template \ + move_t(Args &&...args): el { std::forward(args)... } {} \ + move_t(const move_t &) = delete; \ + \ + move_t(move_t &&other) noexcept: el { std::move(other.el) } { \ + other.el = element_type { init_val }; \ + } \ + \ + move_t & \ + operator=(const move_t &) = delete; \ + \ + move_t & \ + operator=(move_t &&other) { \ + std::swap(el, other.el); \ + return *this; \ + } \ + element_type * \ + operator->() { return ⪙ } \ + const element_type * \ + operator->() const { return ⪙ } \ + \ + inline element_type \ + release() { \ + element_type val = std::move(el); \ + el = element_type { init_val }; \ + return val; \ + } \ + \ + ~move_t() z \ + \ + element_type el; \ } #define KITTY_DECL_CONSTR(x) \ - x(x &&) noexcept = default; \ + x(x &&) noexcept = default; \ x &operator=(x &&) noexcept = default; \ x(); -#define KITTY_DEFAULT_CONSTR_MOVE(x) \ - x(x &&) noexcept = default; \ +#define KITTY_DEFAULT_CONSTR_MOVE(x) \ + x(x &&) noexcept = default; \ x &operator=(x &&) noexcept = default; #define KITTY_DEFAULT_CONSTR_MOVE_THROW(x) \ - x(x &&) = default; \ + x(x &&) = default; \ x &operator=(x &&) = default; \ - x() = default; + x() = default; -#define KITTY_DEFAULT_CONSTR(x) \ - KITTY_DEFAULT_CONSTR_MOVE(x) \ - x(const x &) noexcept = default; \ +#define KITTY_DEFAULT_CONSTR(x) \ + KITTY_DEFAULT_CONSTR_MOVE(x) \ + x(const x &) noexcept = default; \ x &operator=(const x &) = default; -#define TUPLE_2D(a, b, expr) \ - decltype(expr) a##_##b = expr; \ - auto &a = std::get<0>(a##_##b); \ - auto &b = std::get<1>(a##_##b) +#define TUPLE_2D(a, b, expr) \ + decltype(expr) a##_##b = expr; \ + auto &a = std::get<0>(a##_##b); \ + auto &b = std::get<1>(a##_##b) -#define TUPLE_2D_REF(a, b, expr) \ - auto &a##_##b = expr; \ - auto &a = std::get<0>(a##_##b); \ - auto &b = std::get<1>(a##_##b) +#define TUPLE_2D_REF(a, b, expr) \ + auto &a##_##b = expr; \ + auto &a = std::get<0>(a##_##b); \ + auto &b = std::get<1>(a##_##b) -#define TUPLE_3D(a, b, c, expr) \ - decltype(expr) a##_##b##_##c = expr; \ - auto &a = std::get<0>(a##_##b##_##c); \ - auto &b = std::get<1>(a##_##b##_##c); \ - auto &c = std::get<2>(a##_##b##_##c) +#define TUPLE_3D(a, b, c, expr) \ + decltype(expr) a##_##b##_##c = expr; \ + auto &a = std::get<0>(a##_##b##_##c); \ + auto &b = std::get<1>(a##_##b##_##c); \ + auto &c = std::get<2>(a##_##b##_##c) -#define TUPLE_3D_REF(a, b, c, expr) \ - auto &a##_##b##_##c = expr; \ - auto &a = std::get<0>(a##_##b##_##c); \ - auto &b = std::get<1>(a##_##b##_##c); \ - auto &c = std::get<2>(a##_##b##_##c) +#define TUPLE_3D_REF(a, b, c, expr) \ + auto &a##_##b##_##c = expr; \ + auto &a = std::get<0>(a##_##b##_##c); \ + auto &b = std::get<1>(a##_##b##_##c); \ + auto &c = std::get<2>(a##_##b##_##c) #define TUPLE_EL(a, b, expr) \ decltype(expr) a##_ = expr; \ - auto &a = std::get(a##_) + auto &a = std::get(a##_) #define TUPLE_EL_REF(a, b, expr) \ auto &a = std::get(expr) namespace util { -template class X, class... Y> -struct __instantiation_of : public std::false_type {}; + template