move sunshine to src

- this will allow for common cpp workflow files within org
This commit is contained in:
ReenigneArcher
2022-08-07 23:37:57 -04:00
parent 0de52efdb1
commit a4acaf15b0
86 changed files with 3031 additions and 3031 deletions

270
src/audio.cpp Normal file
View File

@@ -0,0 +1,270 @@
#include <thread>
#include <opus/opus_multistream.h>
#include "platform/common.h"
#include "audio.h"
#include "config.h"
#include "main.h"
#include "thread_safe.h"
#include "utility.h"
namespace audio {
using namespace std::literals;
using opus_t = util::safe_ptr<OpusMSEncoder, opus_multistream_encoder_destroy>;
using sample_queue_t = std::shared_ptr<safe::queue_t<std::vector<std::int16_t>>>;
struct audio_ctx_t {
// We want to change the sink for the first stream only
std::unique_ptr<std::atomic_bool> sink_flag;
std::unique_ptr<platf::audio_control_t> control;
bool restore_sink;
platf::sink_t sink;
};
static int start_audio_control(audio_ctx_t &ctx);
static void stop_audio_control(audio_ctx_t &);
int map_stream(int channels, bool quality);
constexpr auto SAMPLE_RATE = 48000;
opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] {
{
SAMPLE_RATE,
2,
1,
1,
platf::speaker::map_stereo,
},
{
SAMPLE_RATE,
6,
4,
2,
platf::speaker::map_surround51,
},
{
SAMPLE_RATE,
6,
6,
0,
platf::speaker::map_surround51,
},
{
SAMPLE_RATE,
8,
5,
3,
platf::speaker::map_surround71,
},
{
SAMPLE_RATE,
8,
8,
0,
platf::speaker::map_surround71,
},
};
auto control_shared = safe::make_shared<audio_ctx_t>(start_audio_control, stop_audio_control);
void encodeThread(sample_queue_t samples, config_t config, void *channel_data) {
auto packets = mail::man->queue<packet_t>(mail::audio_packets);
//FIXME: Pick correct opus_stream_config_t based on config.channels
auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
opus_t opus { opus_multistream_encoder_create(
stream->sampleRate,
stream->channelCount,
stream->streams,
stream->coupledStreams,
stream->mapping,
OPUS_APPLICATION_AUDIO,
nullptr) };
// For some reason, audio is crackling when the encoder is set to constant bitstream.
// We simulate a constant bitstream with OPUS_SET_BITRATE(OPUS_BITRATE_MAX) -->
// which tries to occupy as much space as possible in the packet
opus_multistream_encoder_ctl(opus.get(), OPUS_SET_BITRATE(OPUS_BITRATE_MAX));
auto frame_size = config.packetDuration * stream->sampleRate / 1000;
while(auto sample = samples->pop()) {
buffer_t packet { 1400 }; // 1KB
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;
}
// Even with OPUS_SET_BITRATE(OPUS_BITRATE_MAX), silent packets are smaller than the rest
// Drop silent packets to ensure Moonlight won't complain
// A packet size of 128 seems a reasonable enough threshold
if(bytes < 128) {
BOOST_LOG(verbose) << "Dropped silent packet"sv;
continue;
}
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<bool>(mail::shutdown);
//FIXME: Pick correct opus_stream_config_t based on config.channels
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 priorty:
// 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 client requests audio on the host, don't change the default sink
if(!config.flags[config_t::HOST_AUDIO] && control->set_sink(*sink)) {
return;
}
}
auto samples = std::make_shared<sample_queue_t::element_type>(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<std::int16_t> 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;
case 6:
return SURROUND51 + shift;
case 8:
return SURROUND71 + shift;
}
return STEREO;
}
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<std::atomic_bool>(false);
// The default sink has not been replaced yet.
ctx.restore_sink = false;
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;
}
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

45
src/audio.h Normal file
View File

@@ -0,0 +1,45 @@
#ifndef SUNSHINE_AUDIO_H
#define SUNSHINE_AUDIO_H
#include "thread_safe.h"
#include "utility.h"
namespace audio {
enum stream_config_e : int {
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;
};
extern opus_stream_config_t stream_configs[MAX_STREAM_CONFIG];
struct config_t {
enum flags_e : int {
HIGH_QUALITY,
HOST_AUDIO,
MAX_FLAGS
};
int packetDuration;
int channels;
int mask;
std::bitset<MAX_FLAGS> flags;
};
using buffer_t = util::buffer_t<std::uint8_t>;
using packet_t = std::pair<void *, buffer_t>;
void capture(safe::mail_t mail, config_t config, void *channel_data);
} // namespace audio
#endif

300
src/cbs.cpp Normal file
View File

@@ -0,0 +1,300 @@
extern "C" {
#include <cbs/cbs_h264.h>
#include <cbs/cbs_h265.h>
#include <cbs/video_levels.h>
#include <libavcodec/avcodec.h>
#include <libavutil/pixdesc.h>
}
#include "cbs.h"
#include "main.h"
#include "utility.h"
using namespace std::literals;
namespace cbs {
void close(CodedBitstreamContext *c) {
ff_cbs_close(&c);
}
using ctx_t = util::safe_ptr<CodedBitstreamContext, close>;
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);
}
frag_t &operator=(frag_t &&o) {
std::copy((std::uint8_t *)&o, (std::uint8_t *)(&o + 1), (std::uint8_t *)this);
o.data = nullptr;
o.units = nullptr;
return *this;
};
~frag_t() {
if(data || units) {
ff_cbs_fragment_free(this);
}
}
};
util::buffer_t<std::uint8_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 {};
}
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<std::uint8_t> data { frag.data_size };
std::copy_n(frag.data, frag.data_size, std::begin(data));
return data;
}
util::buffer_t<std::uint8_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<std::uint8_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;
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);
}
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 {};
}
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 {};
}
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<std::uint8_t> read_sps_h264(const AVPacket *packet) {
cbs::ctx_t ctx;
if(ff_cbs_init(&ctx, AV_CODEC_ID_H264, nullptr)) {
return {};
}
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 {};
}
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) {
return false;
}
return true;
}
return ((CodedBitstreamH265Context *)ctx->priv_data)->active_sps->vui_parameters_present_flag;
}
} // namespace cbs

34
src/cbs.h Normal file
View File

@@ -0,0 +1,34 @@
#ifndef SUNSHINE_CBS_H
#define SUNSHINE_CBS_H
#include "utility.h"
struct AVPacket;
struct AVCodecContext;
namespace cbs {
struct nal_t {
util::buffer_t<std::uint8_t> _new;
util::buffer_t<std::uint8_t> old;
};
struct hevc_t {
nal_t vps;
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);
/**
* Check if SPS->VUI is present
*/
bool validate_sps(const AVPacket *packet, int codec_id);
} // namespace cbs
#endif

927
src/config.cpp Normal file
View File

@@ -0,0 +1,927 @@
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iostream>
#include <unordered_map>
#include <boost/asio.hpp>
#include "config.h"
#include "main.h"
#include "utility.h"
#include "platform/common.h"
namespace fs = std::filesystem;
using namespace std::literals;
#define CA_DIR "credentials"
#define PRIVATE_KEY_FILE CA_DIR "/cakey.pem"
#define CERTIFICATE_FILE CA_DIR "/cacert.pem"
#define APPS_JSON_PATH platf::appdata().string() + "/apps.json"
namespace config {
namespace nv {
enum preset_e : int {
_default = 0,
slow,
medium,
fast,
hp,
hq,
bd,
ll_default,
llhq,
llhp,
lossless_default, // lossless presets must be the last ones
lossless_hp,
};
enum rc_e : int {
constqp = 0x0, /**< Constant QP mode */
vbr = 0x1, /**< Variable bitrate mode */
cbr = 0x2, /**< Constant bitrate mode */
cbr_ld_hq = 0x8, /**< low-delay CBR, high quality */
cbr_hq = 0x10, /**< CBR, high quality (slower) */
vbr_hq = 0x20 /**< VBR, high quality (slower) */
};
enum coder_e : int {
_auto = 0,
cabac,
cavlc
};
std::optional<preset_e> preset_from_view(const std::string_view &preset) {
#define _CONVERT_(x) \
if(preset == #x##sv) return x
_CONVERT_(slow);
_CONVERT_(medium);
_CONVERT_(fast);
_CONVERT_(hp);
_CONVERT_(bd);
_CONVERT_(ll_default);
_CONVERT_(llhq);
_CONVERT_(llhp);
_CONVERT_(lossless_default);
_CONVERT_(lossless_hp);
if(preset == "default"sv) return _default;
#undef _CONVERT_
return std::nullopt;
}
std::optional<rc_e> rc_from_view(const std::string_view &rc) {
#define _CONVERT_(x) \
if(rc == #x##sv) return x
_CONVERT_(constqp);
_CONVERT_(vbr);
_CONVERT_(cbr);
_CONVERT_(cbr_hq);
_CONVERT_(vbr_hq);
_CONVERT_(cbr_ld_hq);
#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 nv
namespace amd {
enum quality_e : int {
_default = 0,
speed,
balanced,
};
enum class rc_hevc_e : int {
constqp, /**< Constant QP mode */
vbr_latency, /**< Latency Constrained Variable Bitrate */
vbr_peak, /**< Peak Contrained Variable Bitrate */
cbr, /**< Constant bitrate mode */
};
enum class rc_h264_e : int {
constqp, /**< Constant QP mode */
cbr, /**< Constant bitrate mode */
vbr_peak, /**< Peak Contrained Variable Bitrate */
vbr_latency, /**< Latency Constrained Variable Bitrate */
};
enum coder_e : int {
_auto = 0,
cabac,
cavlc
};
std::optional<quality_e> quality_from_view(const std::string_view &quality) {
#define _CONVERT_(x) \
if(quality == #x##sv) return x
_CONVERT_(speed);
_CONVERT_(balanced);
if(quality == "default"sv) return _default;
#undef _CONVERT_
return std::nullopt;
}
std::optional<int> rc_h264_from_view(const std::string_view &rc) {
#define _CONVERT_(x) \
if(rc == #x##sv) return (int)rc_h264_e::x
_CONVERT_(constqp);
_CONVERT_(vbr_latency);
_CONVERT_(vbr_peak);
_CONVERT_(cbr);
#undef _CONVERT_
return std::nullopt;
}
std::optional<int> rc_hevc_from_view(const std::string_view &rc) {
#define _CONVERT_(x) \
if(rc == #x##sv) return (int)rc_hevc_e::x
_CONVERT_(constqp);
_CONVERT_(vbr_latency);
_CONVERT_(vbr_peak);
_CONVERT_(cbr);
#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 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::llhq,
std::nullopt,
-1 }, // nv
{
amd::balanced,
std::nullopt,
std::nullopt,
-1 }, // amd
{
0,
0,
1,
-1 }, // vt
{}, // 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<double> { 1 / 24.9 }, // key_repeat_period
{
platf::supported_gamepads().front().data(),
platf::supported_gamepads().front().size(),
}, // Default gamepad
};
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,
};
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<class It>
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<std::pair<std::string, std::string>>>
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);
}
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);
}
// 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;
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)));
}
std::unordered_map<std::string, std::string> parse_config(const std::string_view &file_content) {
std::unordered_map<std::string, std::string> 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<std::string, std::string> &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<std::string, std::string> &vars, const std::string &name, std::string &input, const std::vector<std::string_view> &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<std::string, std::string> &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<std::string, std::string> &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<std::string, std::string> &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<int>(val.substr(2));
}
else {
input = util::from_view(val);
}
vars.erase(it);
}
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::optional<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<int>(val.substr(2));
}
else {
input = util::from_view(val);
}
vars.erase(it);
}
template<class F>
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input, F &&f) {
std::string tmp;
string_f(vars, name, tmp);
if(!tmp.empty()) {
input = f(tmp);
}
}
template<class F>
void int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::optional<int> &input, F &&f) {
std::string tmp;
string_f(vars, name, tmp);
if(!tmp.empty()) {
input = f(tmp);
}
}
void int_between_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input, const std::pair<int, int> &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<std::string, std::string> &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<std::string, std::string> &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<std::string, std::string> &vars, const std::string &name, double &input, const std::pair<double, double> &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<std::string, std::string> &vars, const std::string &name, std::vector<std::string> &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_int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<int> &input) {
std::vector<std::string> 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<int>(val.substr(2));
}
else {
tmp = util::from_view(val);
}
input.emplace_back(tmp);
}
}
void map_int_int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::unordered_map<int, int> &input) {
std::vector<int> 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<std::string, std::string> &&vars) {
if(!fs::exists(stream.file_apps.c_str())) {
fs::copy_file(SUNSHINE_CONFIG_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.preset);
string_f(vars, "sw_tune", video.sw.tune);
int_f(vars, "nv_preset", video.nv.preset, nv::preset_from_view);
int_f(vars, "nv_rc", video.nv.rc, nv::rc_from_view);
int_f(vars, "nv_coder", video.nv.coder, nv::coder_from_view);
int_f(vars, "amd_quality", video.amd.quality, amd::quality_from_view);
std::string rc;
string_f(vars, "amd_rc", rc);
int_f(vars, "amd_coder", video.amd.coder, amd::coder_from_view);
if(!rc.empty()) {
video.amd.rc_h264 = amd::rc_h264_from_view(rc);
video.amd.rc_hevc = amd::rc_hevc_from_view(rc);
}
int_f(vars, "vt_coder", video.vt.coder, vt::coder_from_view);
int_f(vars, "vt_software", video.vt.allow_sw, vt::allow_software_from_view);
int_f(vars, "vt_software", video.vt.require_sw, vt::force_software_from_view);
int_f(vars, "vt_realtime", video.vt.realtime, vt::rt_from_view);
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, "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);
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<int>::max() });
if(to != -1) {
stream.ping_timeout = std::chrono::milliseconds(to);
}
int_between_f(vars, "channels", stream.channels, { 1, std::numeric_limits<int>::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<int>::min();
int_f(vars, "back_button_timeout", to);
if(to > std::numeric_limits<int>::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<double>::max() });
if(repeat_frequency > 0) {
config::input.key_repeat_period = std::chrono::duration<double> { 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());
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<std::string, std::string> 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);
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;
}
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));
}
}
}
if(!fs::exists(sunshine.config_file)) {
fs::copy_file(SUNSHINE_CONFIG_DIR "/sunshine.conf", sunshine.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

135
src/config.h Normal file
View File

@@ -0,0 +1,135 @@
#ifndef SUNSHINE_CONFIG_H
#define SUNSHINE_CONFIG_H
#include <bitset>
#include <chrono>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace config {
struct video_t {
// ffmpeg params
int qp; // higher == more compression and less quality
int hevc_mode;
int min_threads; // Minimum number of threads/slices for CPU encoding
struct {
std::string preset;
std::string tune;
} sw;
struct {
std::optional<int> preset;
std::optional<int> rc;
int coder;
} nv;
struct {
std::optional<int> quality;
std::optional<int> rc_h264;
std::optional<int> rc_hevc;
int coder;
} amd;
struct {
int allow_sw;
int require_sw;
int realtime;
int coder;
} vt;
std::string encoder;
std::string adapter_name;
std::string output_name;
bool dwmflush;
};
struct audio_t {
std::string sink;
std::string virtual_sink;
};
struct stream_t {
std::chrono::milliseconds ping_timeout;
std::string file_apps;
int fec_percentage;
// 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;
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 file_state;
std::string external_ip;
std::vector<std::string> resolutions;
std::vector<int> fps;
};
struct input_t {
std::unordered_map<int, int> keybindings;
std::chrono::milliseconds back_button_timeout;
std::chrono::milliseconds key_repeat_delay;
std::chrono::duration<double> key_repeat_period;
std::string gamepad;
};
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 sunshine_t {
int min_log_level;
std::bitset<flag::FLAG_SIZE> flags;
std::string credentials_file;
std::string username;
std::string password;
std::string salt;
std::string config_file;
struct cmd_t {
std::string name;
int argc;
char **argv;
} cmd;
std::uint16_t port;
};
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<std::string, std::string> parse_config(const std::string_view &file_content);
} // namespace config
#endif

666
src/confighttp.cpp Normal file
View File

@@ -0,0 +1,666 @@
//
// Created by TheElixZammuto on 2021-05-09.
// TODO: Authentication, better handling of routes common to nvhttp, cleanup
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
#include "process.h"
#include <filesystem>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <boost/asio/ssl/context.hpp>
#include <Simple-Web-Server/crypto.hpp>
#include <Simple-Web-Server/server_https.hpp>
#include <boost/asio/ssl/context_base.hpp>
#include "config.h"
#include "confighttp.h"
#include "crypto.h"
#include "httpcommon.h"
#include "main.h"
#include "network.h"
#include "nvhttp.h"
#include "platform/common.h"
#include "rtsp.h"
#include "utility.h"
#include "uuid.h"
using namespace std::literals;
namespace confighttp {
namespace fs = std::filesystem;
namespace pt = boost::property_tree;
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
using args_t = SimpleWeb::CaseInsensitiveMultimap;
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::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 << " -- " << 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();
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);
}
void send_redirect(resp_https_t response, req_https_t request, const char *path) {
auto address = request->remote_endpoint_address();
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
const SimpleWeb::CaseInsensitiveMultimap headers {
{ "Location", path }
};
response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers);
}
bool authenticate(resp_https_t response, req_https_t request) {
auto address = request->remote_endpoint_address();
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.<xmlattr>.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();
}
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);
std::string header = read_file(WEB_DIR "header.html");
std::string content = read_file(WEB_DIR "apps.html");
response->write(header + content);
}
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) {
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) {
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);
}
void getFontAwesomeCss(resp_https_t response, req_https_t request) {
print_req(request);
std::string content = read_file(WEB_DIR "fonts/fontawesome-free-web/css/all.min.css");
response->write(content);
}
void getFontAwesomeBrands(resp_https_t response, req_https_t request) {
print_req(request);
std::ifstream in(WEB_DIR "fonts/fontawesome-free-web/webfonts/fa-brands-400.ttf", std::ios::binary);
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "font/ttf");
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
}
void getFontAwesomeSolid(resp_https_t response, req_https_t request) {
print_req(request);
std::ifstream in(WEB_DIR "fonts/fontawesome-free-web/webfonts/fa-solid-900.ttf", std::ios::binary);
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "font/ttf");
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
}
void getBootstrapCss(resp_https_t response, req_https_t request) {
print_req(request);
std::string content = read_file(WEB_DIR "third_party/bootstrap.min.css");
response->write(content);
}
void getBootstrapJs(resp_https_t response, req_https_t request) {
print_req(request);
std::string content = read_file(WEB_DIR "third_party/bootstrap.bundle.min.js");
response->write(content);
}
void getVueJs(resp_https_t response, req_https_t request) {
print_req(request);
std::string content = read_file(WEB_DIR "third_party/vue.js");
response->write(content);
}
void getApps(resp_https_t response, req_https_t request) {
if(!authenticate(response, request)) return;
print_req(request);
std::string content = read_file(config::stream.file_apps.c_str());
response->write(content);
}
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");
}
if(inputTree.get_child("detached").empty()) {
inputTree.erase("detached");
}
auto &apps_node = fileTree.get_child("apps"s);
int index = inputTree.get<int>("index");
inputTree.erase("index");
if(index == -1) {
apps_node.push_back(std::make_pair("", inputTree));
}
else {
//Unfortuantely 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) << "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 deleteApp(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());
});
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 {
//Unfortuantely 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 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);
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<std::string>(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 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<std::string>("currentUsername") : "";
auto newUsername = inputTree.get<std::string>("newUsername");
auto password = inputTree.count("currentPassword") > 0 ? inputTree.get<std::string>("currentPassword") : "";
auto newPassword = inputTree.count("newPassword") > 0 ? inputTree.get<std::string>("newPassword") : "";
auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get<std::string>("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<std::string>("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<bool>(mail::shutdown);
auto port_https = map_port(PORT_HTTPS);
auto ctx = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tls);
ctx->use_certificate_chain_file(config::nvhttp.cert);
ctx->use_private_key_file(config::nvhttp.pkey, boost::asio::ssl::context::pem);
https_server_t server { ctx, 0 };
server.default_resource = 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/apps$"]["POST"] = saveApp;
server.resource["^/api/config$"]["GET"] = getConfig;
server.resource["^/api/config$"]["POST"] = saveConfig;
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["^/images/favicon.ico$"]["GET"] = getFaviconImage;
server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage;
server.resource["^/third_party/bootstrap.min.css$"]["GET"] = getBootstrapCss;
server.resource["^/third_party/bootstrap.bundle.min.js$"]["GET"] = getBootstrapJs;
server.resource["^/fontawesome/css/all.min.css$"]["GET"] = getFontAwesomeCss;
server.resource["^/fontawesome/webfonts/fa-brands-400.ttf$"]["GET"] = getFontAwesomeBrands;
server.resource["^/fontawesome/webfonts/fa-solid-900.ttf$"]["GET"] = getFontAwesomeSolid;
server.resource["^/third_party/vue.js$"]["GET"] = getVueJs;
server.config.reuse_address = true;
server.config.address = "0.0.0.0"s;
server.config.port = port_https;
try {
server.bind();
BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << port_https << "]";
}
catch(boost::system::system_error &err) {
BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << port_https << "]: "sv << err.what();
shutdown_event->raise(true);
return;
}
auto accept_and_run = [&](auto *server) {
try {
server->accept_and_run();
}
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 to 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

21
src/confighttp.h Normal file
View File

@@ -0,0 +1,21 @@
//
// Created by loki on 6/3/19.
//
#ifndef SUNSHINE_CONFIGHTTP_H
#define SUNSHINE_CONFIGHTTP_H
#include <functional>
#include <string>
#include "thread_safe.h"
#define WEB_DIR SUNSHINE_ASSETS_DIR "/web/"
namespace confighttp {
constexpr auto PORT_HTTPS = 1;
void start();
} // namespace confighttp
#endif //SUNSHINE_CONFIGHTTP_H

495
src/crypto.cpp Normal file
View File

@@ -0,0 +1,495 @@
//
// Created by loki on 5/31/19.
//
#include "crypto.h"
#include <openssl/pem.h>
namespace crypto {
using big_num_t = util::safe_ptr<BIGNUM, BN_free>;
//using rsa_t = util::safe_ptr<RSA, RSA_free>;
using asn1_string_t = util::safe_ptr<ASN1_STRING, ASN1_STRING_free>;
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) {
//FIXME: Checking for X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY is a temporary workaround to get mmonlight-embedded to work on the raspberry pi
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
return 1;
// 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.
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());
});
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);
auto err = X509_verify_cert(_cert_ctx.get());
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);
}
}
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<std::uint8_t> &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<char *>(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<std::uint8_t> &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<std::uint8_t> &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<uint8_t, 16> &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;
SHA256_CTX sha256;
SHA256_Init(&sha256);
SHA256_Update(&sha256, plaintext.data(), plaintext.size());
SHA256_Final(hsh.data(), &sha256);
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<uint8_t> 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<uint8_t> 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_t pkey { EVP_PKEY_new() };
big_num_t big_num { BN_new() };
BN_set_word(big_num.get(), RSA_F4);
auto rsa = RSA_new();
RSA_generate_key_ex(rsa, key_bits, big_num.get(), nullptr);
EVP_PKEY_assign_RSA(pkey.get(), rsa);
X509_set_version(x509.get(), 2);
ASN1_INTEGER_set(X509_get_serialNumber(x509.get()), 0);
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);
#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())) };
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());
#endif
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);
X509_set_issuer_name(x509.get(), name);
X509_sign(x509.get(), pkey.get(), EVP_sha256());
return { pem(x509), pem(pkey) };
}
std::vector<uint8_t> 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;
}
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;
}
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()];
}
return value;
}
} // namespace crypto

135
src/crypto.h Normal file
View File

@@ -0,0 +1,135 @@
//
// Created by loki on 6/1/19.
//
#ifndef SUNSHINE_CRYPTO_H
#define SUNSHINE_CRYPTO_H
#include <array>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <openssl/x509.h>
#include "utility.h"
namespace crypto {
struct creds_t {
std::string x509;
std::string pkey;
};
constexpr std::size_t digest_size = 256;
void md_ctx_destroy(EVP_MD_CTX *);
using sha256_t = std::array<std::uint8_t, SHA256_DIGEST_LENGTH>;
using aes_t = std::array<std::uint8_t, 16>;
using x509_t = util::safe_ptr<X509, X509_free>;
using x509_store_t = util::safe_ptr<X509_STORE, X509_STORE_free>;
using x509_store_ctx_t = util::safe_ptr<X509_STORE_CTX, X509_STORE_CTX_free>;
using cipher_ctx_t = util::safe_ptr<EVP_CIPHER_CTX, EVP_CIPHER_CTX_free>;
using md_ctx_t = util::safe_ptr<EVP_MD_CTX, md_ctx_destroy>;
using bio_t = util::safe_ptr<BIO, BIO_free_all>;
using pkey_t = util::safe_ptr<EVP_PKEY, EVP_PKEY_free>;
sha256_t hash(const std::string_view &plaintext);
aes_t gen_aes_key(const std::array<uint8_t, 16> &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);
std::vector<uint8_t> 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);
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!%&()=-" });
class cert_chain_t {
public:
KITTY_DECL_CONSTR(cert_chain_t)
void add(x509_t &&cert);
const char *verify(x509_t::element_type *cert);
private:
std::vector<std::pair<x509_t, x509_store_t>> _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;
}
class cipher_t {
public:
cipher_ctx_t decrypt_ctx;
cipher_ctx_t encrypt_ctx;
aes_t key;
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;
ecb_t(const aes_t &key, bool padding = true);
int encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher);
int decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
};
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);
/**
* 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 decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &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;
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
#endif //SUNSHINE_CRYPTO_H

183
src/httpcommon.cpp Normal file
View File

@@ -0,0 +1,183 @@
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
#include "process.h"
#include <filesystem>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <boost/asio/ssl/context.hpp>
#include <Simple-Web-Server/server_http.hpp>
#include <Simple-Web-Server/server_https.hpp>
#include <boost/asio/ssl/context_base.hpp>
#include "config.h"
#include "crypto.h"
#include "httpcommon.h"
#include "main.h"
#include "network.h"
#include "nvhttp.h"
#include "platform/common.h"
#include "rtsp.h"
#include "utility.h"
#include "uuid.h"
namespace http {
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);
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);
if(clean_slate) {
unique_id = util::uuid_t::generate().string();
auto dir = std::filesystem::temp_directory_path() / "Sushine"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(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(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::write_json(file, outputTree);
}
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;
}
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();
}
return false;
}
int reload_user_creds(const std::string &file) {
pt::ptree inputTree;
try {
pt::read_json(file, inputTree);
config::sunshine.username = inputTree.get<std::string>("username");
config::sunshine.password = inputTree.get<std::string>("password");
config::sunshine.salt = inputTree.get<std::string>("salt");
}
catch(std::exception &e) {
BOOST_LOG(error) << "loading user credentials: "sv << e.what();
return -1;
}
return 0;
}
int create_creds(const std::string &pkey, const std::string &cert) {
fs::path pkey_path = pkey;
fs::path cert_path = cert;
auto creds = crypto::gen_creds("Sunshine Gamestream Host"sv, 2048);
auto pkey_dir = pkey_path;
auto cert_dir = cert_path;
pkey_dir.remove_filename();
cert_dir.remove_filename();
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;
}
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;
}
} // namespace http

19
src/httpcommon.h Normal file
View File

@@ -0,0 +1,19 @@
#include "network.h"
#include "thread_safe.h"
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 reload_user_creds(const std::string &file);
extern std::string unique_id;
extern net::net_e origin_pin_allowed;
extern net::net_e origin_web_ui_allowed;
} // namespace http

686
src/input.cpp Normal file
View File

@@ -0,0 +1,686 @@
//
// Created by loki on 6/20/19.
//
// define uint32_t for <moonlight-common-c/src/Input.h>
#include <cstdint>
extern "C" {
#include <moonlight-common-c/src/Input.h>
}
#include <bitset>
#include <unordered_map>
#include "config.h"
#include "input.h"
#include "main.h"
#include "platform/common.h"
#include "thread_pool.h"
#include "utility.h"
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 ((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;
enum class button_state_e {
NONE,
DOWN,
UP
};
template<std::size_t N>
int alloc_id(std::bitset<N> &gamepad_mask) {
for(int x = 0; x < gamepad_mask.size(); ++x) {
if(!gamepad_mask[x]) {
gamepad_mask[x] = true;
return x;
}
}
return -1;
}
template<std::size_t N>
void free_id(std::bitset<N> &gamepad_mask, int id) {
gamepad_mask[id] = false;
}
static util::TaskPool::task_id_t key_press_repeat_id {};
static std::unordered_map<short, bool> key_press {};
static std::array<std::uint8_t, 5> mouse_press {};
static platf::input_t platf_input;
static std::bitset<platf::MAX_GAMEPADS> 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;
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 erronious 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<input::touch_port_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) },
rumble_queue { std::move(rumble_queue) },
mouse_left_button_timeout {},
touch_port { 0, 0, 0, 0, 0, 0, 1.0f } {}
// Keep track of alt+ctrl+shift key combo
int shortcutFlags;
std::uint16_t active_gamepad_state;
std::vector<gamepad_t> gamepads;
safe::mail_raw_t::event_t<input::touch_port_t> touch_port_event;
platf::rumble_queue_t rumble_queue;
util::ThreadPool::task_id_t mouse_left_button_timeout;
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;
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<int>(mail::switch_display)->raise(keyCode - VK_F1);
return 1;
}
switch(keyCode) {
case 0x4E /* VKEY_N */:
display_cursor = !display_cursor;
return 1;
}
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->action).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(PNV_KEYBOARD_PACKET packet) {
BOOST_LOG(debug)
<< "--begin keyboard packet--"sv << std::endl
<< "keyAction ["sv << util::hex(packet->keyAction).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
<< "--end keyboard 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;
}
constexpr int PACKET_TYPE_SCROLL_OR_KEYBOARD = PACKET_TYPE_SCROLL;
void print(void *input) {
int input_type = util::endian::big(*(int *)input);
switch(input_type) {
case PACKET_TYPE_REL_MOUSE_MOVE:
print((PNV_REL_MOUSE_MOVE_PACKET)input);
break;
case PACKET_TYPE_ABS_MOUSE_MOVE:
print((PNV_ABS_MOUSE_MOVE_PACKET)input);
break;
case PACKET_TYPE_MOUSE_BUTTON:
print((PNV_MOUSE_BUTTON_PACKET)input);
break;
case PACKET_TYPE_SCROLL_OR_KEYBOARD: {
char *tmp_input = (char *)input + 4;
if(tmp_input[0] == 0x0A) {
print((PNV_SCROLL_PACKET)input);
}
else {
print((PNV_KEYBOARD_PACKET)input);
}
break;
}
case PACKET_TYPE_MULTI_CONTROLLER:
print((PNV_MULTI_CONTROLLER_PACKET)input);
break;
}
}
void passthrough(std::shared_ptr<input_t> &input, PNV_REL_MOUSE_MOVE_PACKET packet) {
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_t> &input, PNV_ABS_MOUSE_MOVE_PACKET packet) {
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_t> &input, PNV_MOUSE_BUTTON_PACKET packet) {
auto constexpr BUTTON_RELEASED = 0x09;
auto constexpr BUTTON_LEFT = 0x01;
auto constexpr BUTTON_RIGHT = 0x03;
auto release = packet->action == BUTTON_RELEASED;
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
*
* This can be solved by delaying BUTTON_LEFT, however, any delay on input is undesirable during gaming
* As a compromise, Sunshine will only put delays on BUTTON_LEFT when
* absolute mouse coordinates have been send.
*
* Try to make sure BUTTON_RIGHT gets called before BUTTON_LEFT is released.
*
* 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);
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;
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_t> &input, PNV_KEYBOARD_PACKET packet) {
auto constexpr BUTTON_RELEASED = 0x04;
auto release = packet->keyAction == BUTTON_RELEASED;
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 keycombo 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) {
platf::scroll(platf_input, util::endian::big(packet->scrollAmt1));
}
int updateGamepads(std::vector<gamepad_t> &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;
}
}
}
return 0;
}
void passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET packet) {
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;
}
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 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_t> input, std::vector<std::uint8_t> &&input_data) {
void *payload = input_data.data();
int input_type = util::endian::big(*(int *)payload);
switch(input_type) {
case PACKET_TYPE_REL_MOUSE_MOVE:
passthrough(input, (PNV_REL_MOUSE_MOVE_PACKET)payload);
break;
case PACKET_TYPE_ABS_MOUSE_MOVE:
passthrough(input, (PNV_ABS_MOUSE_MOVE_PACKET)payload);
break;
case PACKET_TYPE_MOUSE_BUTTON:
passthrough(input, (PNV_MOUSE_BUTTON_PACKET)payload);
break;
case PACKET_TYPE_SCROLL_OR_KEYBOARD: {
char *tmp_input = (char *)payload + 4;
if(tmp_input[0] == 0x0A) {
passthrough((PNV_SCROLL_PACKET)payload);
}
else {
passthrough(input, (PNV_KEYBOARD_PACKET)payload);
}
break;
}
case PACKET_TYPE_MULTI_CONTROLLER:
passthrough(input, (PNV_MULTI_CONTROLLER_PACKET)payload);
break;
}
}
void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data) {
task_pool.push(passthrough_helper, input, util::cmove(input_data));
}
void reset(std::shared_ptr<input_t> &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<platf::deinit_t> init() {
platf_input = platf::input();
return std::make_unique<deinit_t>();
}
std::shared_ptr<input_t> alloc(safe::mail_t mail) {
auto input = std::make_shared<input_t>(
mail->event<input::touch_port_t>(mail::touch_port),
mail->queue<platf::rumble_t>(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

35
src/input.h Normal file
View File

@@ -0,0 +1,35 @@
//
// Created by loki on 6/20/19.
//
#ifndef SUNSHINE_INPUT_H
#define SUNSHINE_INPUT_H
#include <functional>
#include "platform/common.h"
#include "thread_safe.h"
namespace input {
struct input_t;
void print(void *input);
void reset(std::shared_ptr<input_t> &input);
void passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data);
[[nodiscard]] std::unique_ptr<platf::deinit_t> init();
std::shared_ptr<input_t> alloc(safe::mail_t mail);
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;
float scalar_inv;
};
} // namespace input
#endif //SUNSHINE_INPUT_H

343
src/main.cpp Normal file
View File

@@ -0,0 +1,343 @@
//
// Created by loki on 5/30/19.
//
#include "process.h"
#include <csignal>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <thread>
#include <boost/log/attributes/clock.hpp>
#include <boost/log/common.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/sinks.hpp>
#include <boost/log/sources/severity_logger.hpp>
#include "config.h"
#include "confighttp.h"
#include "httpcommon.h"
#include "main.h"
#include "nvhttp.h"
#include "rtsp.h"
#include "thread_pool.h"
#include "upnp.h"
#include "version.h"
#include "video.h"
#include "platform/common.h"
extern "C" {
#include <libavutil/log.h>
#include <rs.h>
}
safe::mail_t mail::man;
using namespace std::literals;
namespace bl = boost::log;
util::ThreadPool task_pool;
bl::sources::severity_logger<int> verbose(0); // Dominating output
bl::sources::severity_logger<int> debug(1); // Follow what is happening
bl::sources::severity_logger<int> info(2); // Should be informed about
bl::sources::severity_logger<int> warning(3); // Strange events
bl::sources::severity_logger<int> error(4); // Recoverable errors
bl::sources::severity_logger<int> fatal(5); // Unrecoverable errors
bool display_cursor = true;
using text_sink = bl::sinks::asynchronous_sink<bl::sinks::text_ostream_backend>;
boost::shared_ptr<text_sink> sink;
struct NoDelete {
void operator()(void *) {}
};
BOOST_LOG_ATTRIBUTE_KEYWORD(severity, "Severity", int)
/** Print the help to stdout.
This function prints output to stdout.
*/
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
<< std::endl
<< " Note: The configuration will be created if it doesn't exist."sv << std::endl
<< std::endl
<< " --help | print help"sv << std::endl
<< " --creds username password | set user credentials for the Web manager"sv << std::endl
<< " --version | print the version of sunshine"sv << std::endl
<< std::endl
<< " flags"sv << std::endl
<< " -0 | Read PIN from stdin"sv << std::endl
<< " -1 | Do not load previously saved state and do retain any state after shutdown"sv << std::endl
<< " | Effectively starting as if for the first time without overwriting any pairings with your devices"sv << std::endl
<< " -2 | Force replacement of headers in video stream"sv << std::endl
<< " -p | Enable/Disable UPnP"sv << std::endl
<< std::endl;
}
/** Call the print_help function.
Calls the print_help function and then exits.
*/
namespace help {
int entry(const char *name, int argc, char *argv[]) {
print_help(name);
return 0;
}
} // namespace help
/** Print the version details to stdout.
This function prints the version details to stdout and then exits.
*/
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
void log_flush() {
sink->flush();
}
std::map<int, std::function<void()>> signal_handlers;
void on_signal_forwarder(int sig) {
signal_handlers.at(sig)();
}
template<class FN>
void on_signal(int sig, FN &&fn) {
signal_handlers.emplace(sig, std::forward<FN>(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);
return 0;
}
http::save_user_creds(config::sunshine.credentials_file, argv[0], argv[1]);
return 0;
}
} // namespace gen_creds
std::map<std::string_view, std::function<int(const char *name, int argc, char **argv)>> cmd_to_func {
{ "creds"sv, gen_creds::entry },
{ "help"sv, help::entry },
{ "version"sv, version::entry }
};
int main(int argc, char *argv[]) {
util::TaskPool::task_id_t force_shutdown = nullptr;
bool shutdown_by_interrupt = false;
auto exit_guard = util::fail_guard([&shutdown_by_interrupt, &force_shutdown]() {
if(!shutdown_by_interrupt) {
return;
}
task_pool.cancel(force_shutdown);
std::cout << "Sunshine exited: Press enter to continue"sv << std::endl;
std::string _;
std::getline(std::cin, _);
});
mail::man = std::make_shared<safe::mail_raw_t>();
if(config::parse(argc, argv)) {
return 0;
}
if(config::sunshine.min_log_level >= 1) {
av_log_set_level(AV_LOG_QUIET);
}
else {
av_log_set_level(AV_LOG_DEBUG);
}
sink = boost::make_shared<text_sink>();
boost::shared_ptr<std::ostream> stream { &std::cout, NoDelete {} };
sink->locked_backend()->add_stream(stream);
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"
auto log_level = view.attribute_values()[severity].extract<int>().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;
};
char _date[DATE_BUFFER_SIZE];
std::time_t t = std::time(nullptr);
strftime(_date, DATE_BUFFER_SIZE, "[%Y:%m:%d:%H:%M:%S]: ", std::localtime(&t));
os << _date << log_type << view.attribute_values()[message].extract<std::string>();
});
// Flush after each log record to ensure log file contents on disk isn't stale.
// This is particularly important when running from a Windows service.
sink->locked_backend()->auto_flush(true);
bl::core::get()->add_sink(sink);
auto fg = util::fail_guard(log_flush);
if(!config::sunshine.cmd.name.empty()) {
auto fn = cmd_to_func.find(config::sunshine.cmd.name);
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) {
BOOST_LOG(info) << '\t' << key;
}
return 7;
}
return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.argv);
}
task_pool.start(1);
// Create signal handler after logging has been initialized
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
on_signal(SIGINT, [&shutdown_by_interrupt, &force_shutdown, shutdown_event]() {
BOOST_LOG(info) << "Interrupt handler called"sv;
auto task = []() {
BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv;
log_flush();
std::abort();
};
force_shutdown = task_pool.pushDelayed(task, 10s).task_id;
shutdown_by_interrupt = true;
shutdown_event->raise(true);
});
on_signal(SIGTERM, [&force_shutdown, shutdown_event]() {
BOOST_LOG(info) << "Terminate handler called"sv;
auto task = []() {
BOOST_LOG(fatal) << "10 seconds passed, yet Sunshine's still running: Forcing shutdown"sv;
log_flush();
std::abort();
};
force_shutdown = task_pool.pushDelayed(task, 10s).task_id;
shutdown_event->raise(true);
});
proc::refresh(config::stream.file_apps);
auto deinit_guard = platf::init();
if(!deinit_guard) {
return 4;
}
reed_solomon_init();
auto input_deinit_guard = input::init();
if(video::init()) {
return 2;
}
if(http::init()) {
return 3;
}
std::unique_ptr<platf::deinit_t> mDNS;
auto sync_mDNS = std::async(std::launch::async, [&mDNS]() {
mDNS = platf::publish::start();
});
std::unique_ptr<platf::deinit_t> upnp_unmap;
auto sync_upnp = std::async(std::launch::async, [&upnp_unmap]() {
upnp_unmap = upnp::start();
});
//FIXME: Temporary workaround: Simple-Web_server needs to be updated or replaced
if(shutdown_event->peek()) {
return 0;
}
std::thread httpThread { nvhttp::start };
std::thread configThread { confighttp::start };
stream::rtpThread();
httpThread.join();
configThread.join();
task_pool.stop();
task_pool.join();
return 0;
}
std::string read_file(const char *path) {
if(!std::filesystem::exists(path)) {
return {};
}
std::ifstream in(path);
std::string input;
std::string base64_cert;
while(!in.eof()) {
std::getline(in, input);
base64_cert += input + '\n';
}
return base64_cert;
}
int write_file(const char *path, const std::string_view &contents) {
std::ofstream out(path);
if(!out.is_open()) {
return -1;
}
out << contents;
return 0;
}
std::uint16_t map_port(int port) {
return (std::uint16_t)((int)config::sunshine.port + port);
}

57
src/main.h Normal file
View File

@@ -0,0 +1,57 @@
//
// Created by loki on 12/22/19.
//
#ifndef SUNSHINE_MAIN_H
#define SUNSHINE_MAIN_H
#include <string_view>
#include "thread_pool.h"
#include "thread_safe.h"
#include <boost/log/common.hpp>
extern util::ThreadPool task_pool;
extern bool display_cursor;
extern boost::log::sources::severity_logger<int> verbose;
extern boost::log::sources::severity_logger<int> debug;
extern boost::log::sources::severity_logger<int> info;
extern boost::log::sources::severity_logger<int> warning;
extern boost::log::sources::severity_logger<int> error;
extern boost::log::sources::severity_logger<int> fatal;
void log_flush();
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);
namespace mail {
#define MAIL(x) \
constexpr auto x = std::string_view { #x }
extern safe::mail_t man;
// 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);
#undef MAIL
} // namespace mail
#endif //SUNSHINE_MAIN_H

51
src/move_by_copy.h Normal file
View File

@@ -0,0 +1,51 @@
#ifndef DOSSIER_MOVE_BY_COPY_H
#define DOSSIER_MOVE_BY_COPY_H
#include <utility>
namespace 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 T>
class MoveByCopy {
public:
typedef T move_type;
private:
move_type _to_move;
public:
explicit MoveByCopy(move_type &&to_move) : _to_move(std::move(to_move)) {}
MoveByCopy(MoveByCopy &&other) = default;
MoveByCopy(const MoveByCopy &other) {
*this = other;
}
MoveByCopy &operator=(MoveByCopy &&other) = default;
MoveByCopy &operator=(const MoveByCopy &other) {
this->_to_move = std::move(const_cast<MoveByCopy &>(other)._to_move);
return *this;
}
operator move_type() {
return std::move(_to_move);
}
};
template<class T>
MoveByCopy<T> cmove(T &movable) {
return MoveByCopy<T>(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<class T>
MoveByCopy<T> const_cmove(const T &movable) {
return MoveByCopy<T>(std::move(const_cast<T &>(movable)));
}
} // namespace util
#endif

115
src/network.cpp Normal file
View File

@@ -0,0 +1,115 @@
//
// Created by loki on 12/27/19.
//
#include "network.h"
#include "utility.h"
#include <algorithm>
using namespace std::literals;
namespace net {
// In the format "xxx.xxx.xxx.xxx/x"
std::pair<std::uint32_t, std::uint32_t> ip_block(const std::string_view &ip);
std::vector<std::pair<std::uint32_t, std::uint32_t>> pc_ips {
ip_block("127.0.0.1/32"sv)
};
std::vector<std::tuple<std::uint32_t, std::uint32_t>> 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 = 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<std::uint32_t, std::uint32_t> 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;
}
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;
}
// 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);
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;
if(peer) {
enet_peer_disconnect_now(peer, 0);
}
});
enet_host_destroy(host);
}
} // namespace net

35
src/network.h Normal file
View File

@@ -0,0 +1,35 @@
//
// Created by loki on 12/27/19.
//
#ifndef SUNSHINE_NETWORK_H
#define SUNSHINE_NETWORK_H
#include <tuple>
#include <enet/enet.h>
#include "utility.h"
namespace net {
void free_host(ENetHost *host);
using host_t = util::safe_ptr<ENetHost, free_host>;
using peer_t = ENetPeer *;
using packet_t = util::safe_ptr<ENetPacket, enet_packet_destroy>;
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_address(const std::string_view &view);
host_t host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port);
} // namespace net
#endif //SUNSHINE_NETWORK_H

941
src/nvhttp.cpp Normal file
View File

@@ -0,0 +1,941 @@
//
// Created by loki on 6/3/19.
//
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
#include "process.h"
#include <filesystem>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <boost/asio/ssl/context.hpp>
#include <Simple-Web-Server/server_http.hpp>
#include <Simple-Web-Server/server_https.hpp>
#include <boost/asio/ssl/context_base.hpp>
#include "config.h"
#include "crypto.h"
#include "httpcommon.h"
#include "main.h"
#include "network.h"
#include "nvhttp.h"
#include "platform/common.h"
#include "rtsp.h"
#include "utility.h"
#include "uuid.h"
using namespace std::literals;
namespace nvhttp {
constexpr auto VERSION = "7.1.431.0";
constexpr auto GFE_VERSION = "3.23.0.74";
namespace fs = std::filesystem;
namespace pt = boost::property_tree;
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
using http_server_t = SimpleWeb::Server<SimpleWeb::HTTP>;
struct conf_intern_t {
std::string servercert;
std::string pkey;
} conf_intern;
struct client_t {
std::string uniqueID;
std::vector<std::string> certs;
};
struct pair_session_t {
struct {
std::string uniqueID;
std::string cert;
} client;
std::unique_ptr<crypto::aes_t> cipher_key;
std::vector<uint8_t> clienthash;
std::string serversecret;
std::string serverchallenge;
struct {
util::Either<
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>,
std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>>
response;
std::string salt;
} async_insert_pin;
};
// uniqueID, session
std::unordered_map<std::string, pair_session_t> map_id_sess;
std::unordered_map<std::string, client_t> map_id_client;
using args_t = SimpleWeb::CaseInsensitiveMultimap;
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
using resp_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>;
using req_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Request>;
enum class op_e {
ADD,
REMOVE
};
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;
}
}
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 = util::uuid_t::generate().string();
return;
}
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();
return;
}
auto unique_id_p = root.get_optional<std::string>("root.uniqueid");
if(!unique_id_p) {
// This file doesn't contain moonlight credentials
http::unique_id = util::uuid_t::generate().string();
return;
}
http::unique_id = std::move(*unique_id_p);
auto device_nodes = root.get_child("root.devices");
for(auto &[_, device_node] : device_nodes) {
auto uniqID = device_node.get<std::string>("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<std::string>());
}
}
}
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();
}
}
stream::launch_session_t make_launch_session(bool host_audio, const args_t &args) {
stream::launch_session_t launch_session;
launch_session.host_audio = host_audio;
launch_session.gcm_key = util::from_hex<crypto::aes_t>(args.at("rikey"s), true);
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(args.at("rikeyid"s)));
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);
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.<xmlattr>.status_code", 400);
return;
}
std::string_view salt_view { sess.async_insert_pin.salt.data(), 32 };
auto salt = util::from_hex<std::array<uint8_t, 16>>(salt_view, true);
auto key = crypto::gen_aes_key(salt, pin);
sess.cipher_key = std::make_unique<crypto::aes_t>(key);
tree.put("root.paired", 1);
tree.put("root.plaincert", util::hex_vec(conf_intern.servercert, true));
tree.put("root.<xmlattr>.status_code", 200);
}
void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
auto encrypted_response = util::from_hex_vec(args.at("serverchallengeresp"s), true);
std::vector<uint8_t> 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.<xmlattr>.status_code", 200);
}
void clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
auto challenge = util::from_hex_vec(args.at("clientchallenge"s), true);
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
std::vector<uint8_t> 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<uint8_t> 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.<xmlattr>.status_code", 200);
}
void clientpairingsecret(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, pair_session_t &sess, pt::ptree &tree, const args_t &args) {
auto &client = sess.client;
auto pairingsecret = util::from_hex_vec(args.at("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.<xmlattr>.status_code", 200);
}
template<class T>
struct tunnel;
template<>
struct tunnel<SimpleWeb::HTTPS> {
static auto constexpr to_string = "HTTPS"sv;
};
template<>
struct tunnel<SimpleWeb::HTTP> {
static auto constexpr to_string = "NONE"sv;
};
template<class T>
void print_req(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
BOOST_LOG(debug) << "TUNNEL :: "sv << tunnel<T>::to_string;
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
BOOST_LOG(debug) << "DESTINATION :: "sv << request->path;
for(auto &[name, val] : request->header) {
BOOST_LOG(debug) << name << " -- " << val;
}
BOOST_LOG(debug) << " [--] "sv;
for(auto &[name, val] : request->parse_query_string()) {
BOOST_LOG(debug) << name << " -- " << val;
}
BOOST_LOG(debug) << " [--] "sv;
}
template<class T>
void not_found(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
pt::ptree tree;
tree.put("root.<xmlattr>.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();
response->close_connection_after_response = true;
}
template<class T>
void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
pt::ptree tree;
auto fg = 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.<xmlattr>.status_code", 400);
return;
}
auto uniqID { std::move(args.at("uniqueid"s)) };
auto sess_it = map_id_sess.find(uniqID);
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(args.at("clientcert"s), 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(args.at("salt"s));
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 if(it->second == "pairchallenge"sv) {
tree.put("root.paired", 1);
tree.put("root.<xmlattr>.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.<xmlattr>.status_code", 404);
}
}
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<decltype(async_response.left())>();
// response to the current request
return true;
}
template<class T>
void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
response->close_connection_after_response = true;
auto address = request->remote_endpoint_address();
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<class T>
void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {
print_req<T>(request);
int pair_status = 0;
if constexpr(std::is_same_v<SimpleWeb::HTTPS, T>) {
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;
}
}
}
pt::ptree tree;
tree.put("root.<xmlattr>.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(request->local_endpoint_address()));
tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 1 ? "1869449984" : "0");
tree.put("root.LocalIP", request->local_endpoint_address());
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 >= 0 ? current_appid + 1 : 0);
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<SimpleWeb::HTTPS>(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;
});
auto args = request->parse_query_string();
if(args.find("uniqueid"s) == std::end(args)) {
tree.put("root.<xmlattr>.status_code", 400);
return;
}
auto clientID = args.at("uniqueid"s);
auto client = map_id_client.find(clientID);
if(client == std::end(map_id_client)) {
tree.put("root.<xmlattr>.status_code", 501);
return;
}
auto &apps = tree.add_child("root", pt::ptree {});
apps.put("<xmlattr>.status_code", 200);
int x = 0;
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"s, ++x);
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<SimpleWeb::HTTPS>(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(stream::session_count() == config::stream.channels) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.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.<xmlattr>.status_code", 400);
return;
}
auto appid = util::from_view(args.at("appid")) - 1;
auto current_appid = proc::proc.running();
if(current_appid != -1) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 400);
return;
}
if(appid >= 0) {
auto err = proc::proc.execute(appid);
if(err) {
tree.put("root.<xmlattr>.status_code", err);
tree.put("root.gamesession", 0);
return;
}
}
host_audio = util::from_view(args.at("localAudioPlayMode"));
stream::launch_session_raise(make_launch_session(host_audio, args));
tree.put("root.<xmlattr>.status_code", 200);
tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint_address() + ':' + std::to_string(map_port(stream::RTSP_SETUP_PORT)));
tree.put("root.gamesession", 1);
}
void resume(bool &host_audio, resp_https_t response, req_https_t request) {
print_req<SimpleWeb::HTTPS>(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 negative,
// that is automatically resolved in rtsp_server_t
if(stream::session_count() == config::stream.channels) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
return;
}
auto current_appid = proc::proc.running();
if(current_appid == -1) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.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.<xmlattr>.status_code", 400);
return;
}
stream::launch_session_raise(make_launch_session(host_audio, args));
tree.put("root.<xmlattr>.status_code", 200);
tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint_address() + ':' + std::to_string(map_port(stream::RTSP_SETUP_PORT)));
tree.put("root.resume", 1);
}
void cancel(resp_https_t response, req_https_t request) {
print_req<SimpleWeb::HTTPS>(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(stream::session_count() != 0) {
tree.put("root.resume", 0);
tree.put("root.<xmlattr>.status_code", 503);
return;
}
tree.put("root.cancel", 1);
tree.put("root.<xmlattr>.status_code", 200);
if(proc::proc.running() != -1) {
proc::proc.terminate();
}
}
void appasset(resp_https_t response, req_https_t request) {
print_req<SimpleWeb::HTTPS>(request);
auto args = request->parse_query_string();
auto app_image = proc::proc.get_app_image(util::from_view(args.at("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;
}
void start() {
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
auto port_http = map_port(PORT_HTTP);
auto port_https = map_port(PORT_HTTPS);
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());
auto ctx = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tls);
ctx->use_certificate_chain_file(config::nvhttp.cert);
ctx->use_private_key_file(config::nvhttp.pkey, boost::asio::ssl::context::pem);
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<safe::queue_t<crypto::x509_t>>(30);
ctx->set_verify_callback([](int verified, boost::asio::ssl::verify_context &ctx) {
// To respond with an error message, a connection must be established
return 1;
});
// /resume doesn't get the parameter "localAudioPlayMode"
// /launch will store it in host_audio
bool host_audio {};
https_server_t https_server { ctx, boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once };
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;
}
int verified = 0;
auto fg = util::fail_guard([&]() {
char subject_name[256];
X509_NAME_oneline(X509_get_subject_name(x509), subject_name, sizeof(subject_name));
BOOST_LOG(info) << 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.<xmlattr>.status_code"s, 401);
tree.put("root.<xmlattr>.query"s, req->path);
tree.put("root.<xmlattr>.status_message"s, "The client is not authorized. Certificate verification failed."s);
};
https_server.default_resource = not_found<SimpleWeb::HTTPS>;
https_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTPS>;
https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTPS>(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<SimpleWeb::HTTPS>;
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 = not_found<SimpleWeb::HTTP>;
http_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTP>;
http_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTP>(add_cert, resp, req); };
http_server.resource["^/pin/([0-9]+)$"]["GET"] = pin<SimpleWeb::HTTP>;
http_server.config.reuse_address = true;
http_server.config.address = "0.0.0.0"s;
http_server.config.port = port_http;
try {
https_server.bind();
http_server.bind();
}
catch(boost::system::system_error &err) {
BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << port_http << ", "sv << port_http << "]: "sv << err.what();
shutdown_event->raise(true);
return;
}
auto accept_and_run = [&](auto *http_server) {
try {
http_server->accept_and_run();
}
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 to 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();
}
void erase_all_clients() {
map_id_client.clear();
save_state();
}
} // namespace nvhttp

20
src/nvhttp.h Normal file
View File

@@ -0,0 +1,20 @@
//
// Created by loki on 6/3/19.
//
#ifndef SUNSHINE_NVHTTP_H
#define SUNSHINE_NVHTTP_H
#include "thread_safe.h"
#include <string>
namespace nvhttp {
constexpr auto PORT_HTTP = 0;
constexpr auto PORT_HTTPS = -5;
void start();
bool pin(std::string pin);
void erase_all_clients();
} // namespace nvhttp
#endif //SUNSHINE_NVHTTP_H

315
src/platform/common.h Normal file
View File

@@ -0,0 +1,315 @@
//
// Created by loki on 6/21/19.
//
#ifndef SUNSHINE_COMMON_H
#define SUNSHINE_COMMON_H
#include <bitset>
#include <filesystem>
#include <functional>
#include <mutex>
#include <string>
#include "src/thread_safe.h"
#include "src/utility.h"
struct sockaddr;
struct AVFrame;
namespace platf {
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;
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 } {}
std::uint16_t id;
std::uint16_t lowfreq;
std::uint16_t highfreq;
};
using rumble_queue_t = safe::mail_raw_t::queue_t<rumble_t>;
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,
LOW_FREQUENCY,
BACK_LEFT,
BACK_RIGHT,
SIDE_LEFT,
SIDE_RIGHT,
};
} // namespace speaker
enum class mem_type_e {
system,
vaapi,
dxgi,
cuda,
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;
#define _CONVERT(x) \
case pix_fmt_e::x: \
return #x##sv
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_t> 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) {
std::abort(); // ^ This function must never be called
return -1;
};
virtual void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) {};
virtual ~hwdevice_t() = default;
};
enum class capture_e : int {
ok,
reinit,
timeout,
error
};
class display_t {
public:
/**
* When display has a new image ready, this callback will be called with the new image.
*
* On Break Request -->
* Returns nullptr
*
* On Success -->
* 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_t>(std::shared_ptr<img_t> &img)>;
display_t() noexcept : offset_x { 0 }, offset_y { 0 } {}
/**
* snapshot_cb --> the callback
* std::shared_ptr<img_t> img --> The first image to use
* bool *cursor --> A pointer to the flag that indicates wether the cursor should be captured as well
*
* Returns either:
* capture_e::ok when stopping
* 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_t> img, bool *cursor) = 0;
virtual std::shared_ptr<img_t> alloc_img() = 0;
virtual int dummy_img(img_t *img) = 0;
virtual std::shared_ptr<hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) {
return std::make_shared<hwdevice_t>();
}
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;
int width, height;
};
class mic_t {
public:
virtual capture_e sample(std::vector<std::int16_t> &frame_buffer) = 0;
virtual ~mic_t() = default;
};
class audio_control_t {
public:
virtual int set_sink(const std::string &sink) = 0;
virtual std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) = 0;
virtual std::optional<sink_t> sink_info() = 0;
virtual ~audio_control_t() = default;
};
void freeInput(void *);
using input_t = util::safe_ptr<void, freeInput>;
std::filesystem::path appdata();
std::string get_mac_address(const std::string_view &address);
std::string from_sockaddr(const sockaddr *const);
std::pair<std::uint16_t, std::string> from_sockaddr_ex(const sockaddr *const);
std::unique_ptr<audio_control_t> 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.
*
* framerate --> The peak number of images per second
*
* Returns display_t based on hwdevice_type
*/
std::shared_ptr<display_t> display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
// A list of names of displays accepted as display_name with the mem_type_e
std::vector<std::string> display_names(mem_type_e hwdevice_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 keyboard(input_t &input, uint16_t modcode, bool release);
void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state);
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<deinit_t> start();
}
[[nodiscard]] std::unique_ptr<deinit_t> init();
std::vector<std::string_view> &supported_gamepads();
} // namespace platf
#endif //SUNSHINE_COMMON_H

View File

@@ -0,0 +1,506 @@
//
// Created by loki on 5/16/21.
//
#include <bitset>
#include <sstream>
#include <boost/regex.hpp>
#include <pulse/error.h>
#include <pulse/pulseaudio.h>
#include <pulse/simple.h>
#include "src/platform/common.h"
#include "src/config.h"
#include "src/main.h"
#include "src/thread_safe.h"
namespace platf {
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<pa_simple, pa_simple_free> mic;
capture_e sample(std::vector<std::int16_t> &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<mic_t> 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<mic_attr_t>();
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;
log_flush();
std::abort();
}
return mic;
}
namespace pa {
template<bool B, class T>
struct add_const_helper;
template<class T>
struct add_const_helper<true, T> {
using type = const std::remove_pointer_t<T> *;
};
template<class T>
struct add_const_helper<false, T> {
using type = const T *;
};
template<class T>
using add_const_t = typename add_const_helper<std::is_pointer_v<T>, T>::type;
template<class T>
void pa_free(T *p) {
pa_xfree(p);
}
using ctx_t = util::safe_ptr<pa_context, pa_context_unref>;
using loop_t = util::safe_ptr<pa_mainloop, pa_mainloop_free>;
using op_t = util::safe_ptr<pa_operation, pa_operation_unref>;
using string_t = util::safe_ptr<char, pa_free<char>>;
template<class T>
using cb_simple_t = std::function<void(ctx_t::pointer, add_const_t<T> i)>;
template<class T>
void cb(ctx_t::pointer ctx, add_const_t<T> i, void *userdata) {
auto &f = *(cb_simple_t<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<class T>
using cb_t = std::function<void(ctx_t::pointer, add_const_t<T> i, int eol)>;
template<class T>
void cb(ctx_t::pointer ctx, add_const_t<T> i, int eol, void *userdata) {
auto &f = *(cb_t<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<int> *)userdata;
alarm->ring(i);
}
void ctx_state_cb(ctx_t::pointer ctx, void *userdata) {
auto &f = *(std::function<void(ctx_t::pointer)> *)userdata;
f(ctx);
}
void success_cb(ctx_t::pointer ctx, int status, void *userdata) {
assert(userdata != nullptr);
auto alarm = (safe::alarm_raw_t<int> *)userdata;
alarm->ring(status ? 0 : 1);
}
class server_t : public audio_control_t {
enum ctx_event_e : int {
ready,
terminated,
failed
};
public:
loop_t loop;
ctx_t ctx;
std::string requested_sink;
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<safe::event_t<ctx_event_e>> events;
std::unique_ptr<std::function<void(ctx_t::pointer)>> events_cb;
std::thread worker;
int init() {
events = std::make_unique<safe::event_t<ctx_event_e>>();
loop.reset(pa_mainloop_new());
ctx.reset(pa_context_new(pa_mainloop_get_api(loop.get()), "sunshine"));
events_cb = std::make_unique<std::function<void(ctx_t::pointer)>>([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;
}
});
pa_context_set_state_callback(ctx.get(), ctx_state_cb, events_cb.get());
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;
}
worker = std::thread {
[](loop_t::pointer loop) {
int retval;
auto status = pa_mainloop_run(loop, &retval);
if(status < 0) {
BOOST_LOG(fatal) << "Couldn't run pulseaudio main loop"sv;
log_flush();
std::abort();
}
},
loop.get()
};
auto event = events->pop();
if(event == failed) {
return -1;
}
return 0;
}
int load_null(const char *name, const std::uint8_t *channel_mapping, int channels) {
auto alarm = safe::make_alarm<int>();
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<int>();
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_t> 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<int>();
sink_t sink;
// Count of all virtual sinks that are created by us
int nullcount = 0;
cb_t<pa_sink_info *> 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<pa_sink_info *>, &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();
if(sink_name.empty()) {
BOOST_LOG(warning) << "Couldn't find an active sink"sv;
}
else {
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(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 = "@DEFAULT_SINK@"s;
auto alarm = safe::make_alarm<int>();
cb_simple_t<pa_server_info *> 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);
}
sink_name = server_info->default_sink_name;
alarm->ring(0);
};
op_t server_op { pa_context_get_server_info(ctx.get(), cb<pa_server_info *>, &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 = "@DEFAULT_MONITOR@"s;
auto alarm = safe::make_alarm<int>();
cb_t<pa_sink_info *> 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<pa_sink_info *>, &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<mic_t> 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<int>();
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
std::unique_ptr<audio_control_t> audio_control() {
auto audio = std::make_unique<pa::server_t>();
if(audio->init()) {
return nullptr;
}
return audio;
}
} // namespace platf

717
src/platform/linux/cuda.cpp Normal file
View File

@@ -0,0 +1,717 @@
#include <bitset>
#include <NvFBC.h>
#include <ffnvcodec/dynlink_loader.h>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/hwcontext_cuda.h>
#include <libavutil/imgutils.h>
}
#include "cuda.h"
#include "graphics.h"
#include "src/main.h"
#include "src/utility.h"
#include "wayland.h"
#define SUNSHINE_STRINGVIEW_HELPER(x) x##sv
#define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x)
#define CU_CHECK(x, y) \
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<CudaFunctions, cff>;
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);
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;
}
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;
}
data = (void *)0x1;
width = in_width;
height = in_height;
return 0;
}
int set_frame(AVFrame *frame) override {
this->hwframe.reset(frame);
this->frame = frame;
auto hwframe_ctx = (AVHWFramesContext *)frame->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(av_hwframe_get_buffer(frame->hw_frames_ctx, frame, 0)) {
BOOST_LOG(error) << "Couldn't get hwframe for NVENC"sv;
return -1;
}
auto cuda_ctx = (AVCUDADeviceContext *)hwframe_ctx->device_ctx->hwctx;
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;
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<std::uint8_t> 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) {
if(cuda_t::set_frame(frame)) {
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<platf::hwdevice_t> make_hwdevice(int width, int height, bool vram) {
if(init()) {
return nullptr;
}
std::shared_ptr<cuda_t> cuda;
if(vram) {
cuda = std::make_shared<cuda_vram_t>();
}
else {
cuda = std::make_shared<cuda_ram_t>();
}
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<std::tuple<dyn::apiproc *, const char *>> 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, &params)) {
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, &params)) {
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<handle_t> make() {
NVFBC_CREATE_HANDLE_PARAMS params { NVFBC_CREATE_HANDLE_PARAMS_VER };
handle_t handle;
auto status = func.nvFBCCreateHandle(&handle.handle, &params);
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<NVFBC_GET_STATUS_PARAMS> status() {
NVFBC_GET_STATUS_PARAMS params { NVFBC_GET_STATUS_PARAMS_VER };
auto status = func.nvFBCGetStatus(handle, &params);
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, &params)) {
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, &params)) {
BOOST_LOG(error) << "Couldn't destroy session handle: "sv << func.nvFBCGetLastErrorStr(handle);
}
handle_flags[SESSION_HANDLE] = false;
return 0;
}
~handle_t() {
reset();
}
std::bitset<MAX_FLAGS> handle_flags;
NVFBC_SESSION_HANDLE handle;
};
class display_t : public platf::display_t {
public:
int init(const std::string_view &display_name, int framerate) {
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 } / 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 */ / 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<platf::img_t> 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:
std::this_thread::sleep_for(1ms);
continue;
case platf::capture_e::ok:
img = snapshot_cb(img);
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(((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<platf::hwdevice_t> make_hwdevice(platf::pix_fmt_e pix_fmt) override {
return ::cuda::make_hwdevice(width, height, true);
}
std::shared_ptr<platf::img_t> alloc_img() override {
auto img = std::make_shared<cuda::img_t>();
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
namespace platf {
std::shared_ptr<display_t> nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
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 display = std::make_shared<cuda::nvfbc::display_t>();
if(display->init(display_name, framerate)) {
return nullptr;
}
return display;
}
std::vector<std::string> nvfbc_display_names() {
if(cuda::init() || cuda::nvfbc::init()) {
return {};
}
std::vector<std::string> 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

331
src/platform/linux/cuda.cu Normal file
View File

@@ -0,0 +1,331 @@
// #include <algorithm>
#include <helper_math.h>
#include <limits>
#include <memory>
#include <optional>
#include <string_view>
#include "cuda.h"
using namespace std::literals;
#define SUNSHINE_STRINGVIEW_HELPER(x) x##sv
#define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x)
#define CU_CHECK(x, y) \
if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return -1
#define CU_CHECK_VOID(x, y) \
if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return;
#define CU_CHECK_PTR(x, y) \
if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return nullptr;
#define CU_CHECK_OPT(x, y) \
if(check((x), SUNSHINE_STRINGVIEW(y ": "))) return std::nullopt;
#define CU_CHECK_IGNORE(x, y) \
check((x), SUNSHINE_STRINGVIEW(y ": "))
using namespace std::literals;
//////////////////// Special desclarations
/**
* NVCC segfaults when including <chrono>
* Therefore, some declarations need to be added explicitely
*/
namespace platf {
struct img_t {
public:
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;
};
} // namespace platf
namespace video {
using __float4 = float[4];
using __float3 = float[3];
using __float2 = float[2];
struct __attribute__((__aligned__(16))) color_t {
float4 color_vec_y;
float4 color_vec_u;
float4 color_vec_v;
float2 range_y;
float2 range_uv;
};
struct __attribute__((__aligned__(16))) color_extern_t {
__float4 color_vec_y;
__float4 color_vec_u;
__float4 color_vec_v;
__float2 range_y;
__float2 range_uv;
};
static_assert(sizeof(video::color_t) == sizeof(video::color_extern_t), "color matrix struct mismatch");
extern color_t colors[4];
} // namespace video
//////////////////// End special declarations
namespace cuda {
auto constexpr INVALID_TEXTURE = std::numeric_limits<cudaTextureObject_t>::max();
template<class T>
inline T div_align(T l, T r) {
return (l + r - 1) / r;
}
void pass_error(const std::string_view &sv, const char *name, const char *description);
inline static int check(cudaError_t result, const std::string_view &sv) {
if(result) {
auto name = cudaGetErrorName(result);
auto description = cudaGetErrorString(result);
pass_error(sv, name, description);
return -1;
}
return 0;
}
template<class T>
ptr_t make_ptr() {
void *p;
CU_CHECK_PTR(cudaMalloc(&p, sizeof(T)), "Couldn't allocate color matrix");
ptr_t ptr { p };
return ptr;
}
void freeCudaPtr_t::operator()(void *ptr) {
CU_CHECK_IGNORE(cudaFree(ptr), "Couldn't free cuda device pointer");
}
void freeCudaStream_t::operator()(cudaStream_t ptr) {
CU_CHECK_IGNORE(cudaStreamDestroy(ptr), "Couldn't free cuda stream");
}
stream_t make_stream(int flags) {
cudaStream_t stream;
if(!flags) {
CU_CHECK_PTR(cudaStreamCreate(&stream), "Couldn't create cuda stream");
}
else {
CU_CHECK_PTR(cudaStreamCreateWithFlags(&stream, flags), "Couldn't create cuda stream with flags");
}
return stream_t { stream };
}
inline __device__ float3 bgra_to_rgb(uchar4 vec) {
return make_float3((float)vec.z, (float)vec.y, (float)vec.x);
}
inline __device__ float3 bgra_to_rgb(float4 vec) {
return make_float3(vec.z, vec.y, vec.x);
}
inline __device__ float2 calcUV(float3 pixel, const video::color_t *const color_matrix) {
float4 vec_u = color_matrix->color_vec_u;
float4 vec_v = color_matrix->color_vec_v;
float u = dot(pixel, make_float3(vec_u)) + vec_u.w;
float v = dot(pixel, make_float3(vec_v)) + vec_v.w;
u = u * color_matrix->range_uv.x + color_matrix->range_uv.y;
v = (v * color_matrix->range_uv.x + color_matrix->range_uv.y) * 224.0f / 256.0f + 0.0625f;
return make_float2(u, v);
}
inline __device__ float calcY(float3 pixel, const video::color_t *const color_matrix) {
float4 vec_y = color_matrix->color_vec_y;
return (dot(pixel, make_float3(vec_y)) + vec_y.w) * color_matrix->range_y.x + color_matrix->range_y.y;
}
__global__ void RGBA_to_NV12(
cudaTextureObject_t srcImage, std::uint8_t *dstY, std::uint8_t *dstUV,
std::uint32_t dstPitchY, std::uint32_t dstPitchUV,
float scale, const viewport_t viewport, const video::color_t *const color_matrix) {
int idX = (threadIdx.x + blockDim.x * blockIdx.x) * 2;
int idY = (threadIdx.y + blockDim.y * blockIdx.y);
if(idX >= viewport.width) return;
if(idY >= viewport.height) return;
float x = idX * scale;
float y = idY * scale;
idX += viewport.offsetX;
idY += viewport.offsetY;
dstY = dstY + idX + idY * dstPitchY;
dstUV = dstUV + idX + (idY / 2 * dstPitchUV);
float3 rgb_l = bgra_to_rgb(tex2D<float4>(srcImage, x, y));
float3 rgb_r = bgra_to_rgb(tex2D<float4>(srcImage, x + scale, y));
float2 uv = calcUV((rgb_l + rgb_r) * 0.5f, color_matrix) * 256.0f;
dstUV[0] = uv.x;
dstUV[1] = uv.y;
dstY[0] = calcY(rgb_l, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visisble
dstY[1] = calcY(rgb_r, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visisble
}
int tex_t::copy(std::uint8_t *src, int height, int pitch) {
CU_CHECK(cudaMemcpy2DToArray(array, 0, 0, src, pitch, pitch, height, cudaMemcpyDeviceToDevice), "Couldn't copy to cuda array from deviceptr");
return 0;
}
std::optional<tex_t> tex_t::make(int height, int pitch) {
tex_t tex;
auto format = cudaCreateChannelDesc<uchar4>();
CU_CHECK_OPT(cudaMallocArray(&tex.array, &format, pitch, height, cudaArrayDefault), "Couldn't allocate cuda array");
cudaResourceDesc res {};
res.resType = cudaResourceTypeArray;
res.res.array.array = tex.array;
cudaTextureDesc desc {};
desc.readMode = cudaReadModeNormalizedFloat;
desc.filterMode = cudaFilterModePoint;
desc.normalizedCoords = false;
std::fill_n(std::begin(desc.addressMode), 2, cudaAddressModeClamp);
CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.point, &res, &desc, nullptr), "Couldn't create cuda texture that uses point interpolation");
desc.filterMode = cudaFilterModeLinear;
CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.linear, &res, &desc, nullptr), "Couldn't create cuda texture that uses linear interpolation");
return std::move(tex);
}
tex_t::tex_t() : array {}, texture { INVALID_TEXTURE } {}
tex_t::tex_t(tex_t &&other) : array { other.array }, texture { other.texture } {
other.array = 0;
other.texture.point = INVALID_TEXTURE;
other.texture.linear = INVALID_TEXTURE;
}
tex_t &tex_t::operator=(tex_t &&other) {
std::swap(array, other.array);
std::swap(texture, other.texture);
return *this;
}
tex_t::~tex_t() {
if(texture.point != INVALID_TEXTURE) {
CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.point), "Couldn't deallocate cuda texture that uses point interpolation");
texture.point = INVALID_TEXTURE;
}
if(texture.linear != INVALID_TEXTURE) {
CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.linear), "Couldn't deallocate cuda texture that uses linear interpolation");
texture.linear = INVALID_TEXTURE;
}
if(array) {
CU_CHECK_IGNORE(cudaFreeArray(array), "Couldn't deallocate cuda array");
array = cudaArray_t {};
}
}
sws_t::sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix)
: threadsPerBlock { threadsPerBlock }, color_matrix { std::move(color_matrix) } {
// Ensure aspect ratio is maintained
auto scalar = std::fminf(out_width / (float)in_width, out_height / (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_height - out_height_f) / 2;
viewport.width = out_width_f;
viewport.height = out_height_f;
viewport.offsetX = offsetX_f;
viewport.offsetY = offsetY_f;
scale = 1.0f / scalar;
}
std::optional<sws_t> sws_t::make(int in_width, int in_height, int out_width, int out_height, int pitch) {
cudaDeviceProp props;
int device;
CU_CHECK_OPT(cudaGetDevice(&device), "Couldn't get cuda device");
CU_CHECK_OPT(cudaGetDeviceProperties(&props, device), "Couldn't get cuda device properties");
auto ptr = make_ptr<video::color_t>();
if(!ptr) {
return std::nullopt;
}
return std::make_optional<sws_t>(in_width, in_height, out_width, out_height, pitch, props.maxThreadsPerMultiProcessor / props.maxBlocksPerMultiProcessor, std::move(ptr));
}
int sws_t::convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream) {
return convert(Y, UV, pitchY, pitchUV, texture, stream, viewport);
}
int sws_t::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) {
int threadsX = viewport.width / 2;
int threadsY = viewport.height;
dim3 block(threadsPerBlock);
dim3 grid(div_align(threadsX, threadsPerBlock), threadsY);
RGBA_to_NV12<<<grid, block, 0, stream>>>(texture, Y, UV, pitchY, pitchUV, scale, viewport, (video::color_t *)color_matrix.get());
return CU_CHECK_IGNORE(cudaGetLastError(), "RGBA_to_NV12 failed");
}
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
default:
color_p = &video::colors[0];
};
if(color_range > 1) {
// Full range
++color_p;
}
CU_CHECK_IGNORE(cudaMemcpy(color_matrix.get(), color_p, sizeof(video::color_t), cudaMemcpyHostToDevice), "Couldn't copy color matrix to cuda");
}
int sws_t::load_ram(platf::img_t &img, cudaArray_t array) {
return CU_CHECK_IGNORE(cudaMemcpy2DToArray(array, 0, 0, img.data, img.row_pitch, img.width * img.pixel_pitch, img.height, cudaMemcpyHostToDevice), "Couldn't copy to cuda array");
}
} // namespace cuda

107
src/platform/linux/cuda.h Normal file
View File

@@ -0,0 +1,107 @@
#if !defined(SUNSHINE_PLATFORM_CUDA_H) && defined(SUNSHINE_BUILD_CUDA)
#define SUNSHINE_PLATFORM_CUDA_H
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace platf {
class hwdevice_t;
class img_t;
} // namespace platf
namespace cuda {
namespace nvfbc {
std::vector<std::string> display_names();
}
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, bool vram);
int init();
} // namespace cuda
typedef struct cudaArray *cudaArray_t;
#if !defined(__CUDACC__)
typedef struct CUstream_st *cudaStream_t;
typedef unsigned long long cudaTextureObject_t;
#else /* defined(__CUDACC__) */
typedef __location__(device_builtin) struct CUstream_st *cudaStream_t;
typedef __location__(device_builtin) unsigned long long cudaTextureObject_t;
#endif /* !defined(__CUDACC__) */
namespace cuda {
class freeCudaPtr_t {
public:
void operator()(void *ptr);
};
class freeCudaStream_t {
public:
void operator()(cudaStream_t ptr);
};
using ptr_t = std::unique_ptr<void, freeCudaPtr_t>;
using stream_t = std::unique_ptr<CUstream_st, freeCudaStream_t>;
stream_t make_stream(int flags = 0);
struct viewport_t {
int width, height;
int offsetX, offsetY;
};
class tex_t {
public:
static std::optional<tex_t> make(int height, int pitch);
tex_t();
tex_t(tex_t &&);
tex_t &operator=(tex_t &&other);
~tex_t();
int copy(std::uint8_t *src, int height, int pitch);
cudaArray_t array;
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);
/**
* 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<sws_t> 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);
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range);
int load_ram(platf::img_t &img, cudaArray_t array);
ptr_t color_matrix;
int threadsPerBlock;
viewport_t viewport;
float scale;
};
} // namespace cuda
#endif

View File

@@ -0,0 +1,866 @@
#include "graphics.h"
#include "src/video.h"
#include <fcntl.h>
// I want to have as little build dependencies as possible
// There aren't that many DRM_FORMAT I need to use, so define them here
//
// 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 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 */
#define DRM_FORMAT_MOD_INVALID fourcc_mod_code(0, ((1ULL << 56) - 1))
#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/opengl"
using namespace std::literals;
namespace gl {
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, std::string> 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<GLint> &&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<std::uint8_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, std::string> 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<buffer_t> program_t::uniform(const char *block, std::pair<const char *, std::string_view> *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<GLint> 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 << ']';
}
}
if(error_flag) {
return std::nullopt;
}
ctx.GetActiveUniformsiv(handle(), count, indices, GL_UNIFORM_OFFSET, offsets.begin());
util::buffer_t<std::uint8_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;
}
}
std::vector<std::tuple<GLADapiproc *, const char *>> funcs {
{ (GLADapiproc *)&device_destroy, "gbm_device_destroy" },
{ (GLADapiproc *)&create_device, "gbm_create_device" },
};
if(dyn::load(handle, funcs)) {
return -1;
}
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<gbm::gbm_t::pointer, wl_display *, _XDisplay *> 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",
};
for(auto ext : extensions) {
if(!std::strstr(extension_st, ext)) {
BOOST_LOG(error) << "Missing extension: ["sv << ext << ']';
return nullptr;
}
}
return display;
}
std::optional<ctx_t> 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<rgb_t> 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<nv12_t> import_target(display_t::pointer egl_display, std::array<file_t, nv12_img_t::num_fds> &&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
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> 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<std::uint64_t>::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<gl::shader_t, std::string> 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<const char *, std::string_view> 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> 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);
}

View File

@@ -0,0 +1,319 @@
#ifndef SUNSHINE_PLATFORM_LINUX_OPENGL_H
#define SUNSHINE_PLATFORM_LINUX_OPENGL_H
#include <optional>
#include <string_view>
#include <glad/egl.h>
#include <glad/gl.h>
#include "misc.h"
#include "src/main.h"
#include "src/platform/common.h"
#include "src/utility.h"
#define SUNSHINE_STRINGIFY_HELPER(x) #x
#define SUNSHINE_STRINGIFY(x) SUNSHINE_STRINGIFY_HELPER(x)
#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);
// X11 Display
extern "C" struct _XDisplay;
struct AVFrame;
void free_frame(AVFrame *frame);
using frame_t = util::safe_ptr<AVFrame, free_frame>;
namespace gl {
extern GladGLContext ctx;
void drain_errors(const std::string_view &prefix);
class tex_t : public util::buffer_t<GLuint> {
using util::buffer_t<GLuint>::buffer_t;
public:
tex_t(tex_t &&) = default;
tex_t &operator=(tex_t &&) = default;
~tex_t();
static tex_t make(std::size_t count);
};
class frame_buf_t : public util::buffer_t<GLuint> {
using util::buffer_t<GLuint>::buffer_t;
public:
frame_buf_t(frame_buf_t &&) = default;
frame_buf_t &operator=(frame_buf_t &&) = default;
~frame_buf_t();
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);
++x;
}
return;
}
template<class It>
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;
}
int x = 0;
std::for_each(it_begin, it_end, [&](auto tex) {
ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[x]);
ctx.BindTexture(GL_TEXTURE_2D, tex);
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);
};
class shader_t {
KITTY_USING_MOVE_T(shader_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {
if(el != std::numeric_limits<GLuint>::max()) {
ctx.DeleteShader(el);
}
});
public:
std::string err_str();
static util::Either<shader_t, std::string> compile(const std::string_view &source, GLenum type);
GLuint handle() const;
private:
shader_internal_t _shader;
};
class buffer_t {
KITTY_USING_MOVE_T(buffer_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {
if(el != std::numeric_limits<GLuint>::max()) {
ctx.DeleteBuffers(1, &el);
}
});
public:
static buffer_t make(util::buffer_t<GLint> &&offsets, const char *block, const std::string_view &data);
GLuint handle() 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);
private:
const char *_block;
std::size_t _size;
util::buffer_t<GLint> _offsets;
buffer_internal_t _buffer;
};
class program_t {
KITTY_USING_MOVE_T(program_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {
if(el != std::numeric_limits<GLuint>::max()) {
ctx.DeleteProgram(el);
}
});
public:
std::string err_str();
static util::Either<program_t, std::string> link(const shader_t &vert, const shader_t &frag);
void bind(const buffer_t &buffer);
std::optional<buffer_t> uniform(const char *block, std::pair<const char *, std::string_view> *members, std::size_t count);
GLuint handle() const;
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);
extern device_destroy_fn device_destroy;
extern create_device_fn create_device;
using gbm_t = util::dyn_safe_ptr<device, &device_destroy>;
int init();
} // namespace gbm
namespace egl {
using display_t = util::dyn_safe_ptr_v2<void, EGLBoolean, &eglTerminate>;
struct rgb_img_t {
display_t::pointer display;
EGLImage xrgb8;
gl::tex_t tex;
};
struct nv12_img_t {
display_t::pointer display;
EGLImage r8;
EGLImage bg88;
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;
std::array<file_t, num_fds> fds;
};
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);
}
if(el.bg88) {
eglDestroyImage(el.display, el.bg88);
}
});
KITTY_USING_MOVE_T(ctx_t, (std::tuple<display_t::pointer, EGLContext>), , {
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];
};
display_t make_display(std::variant<gbm::gbm_t::pointer, wl_display *, _XDisplay *> native_display);
std::optional<ctx_t> make_ctx(display_t::pointer display);
std::optional<rgb_t> import_source(
display_t::pointer egl_display,
const surface_descriptor_t &xrgb);
std::optional<nv12_t> import_target(
display_t::pointer egl_display,
std::array<file_t, nv12_img_t::num_fds> &&fds,
const surface_descriptor_t &r8, const surface_descriptor_t &gr88);
class cursor_t : public platf::img_t {
public:
int x, y;
unsigned long serial;
std::vector<std::uint8_t> buffer;
};
// 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]);
sd.fds[x] = -1;
}
}
}
surface_descriptor_t sd;
// Increment sequence when new rgb_t needs to be created
std::uint64_t sequence;
};
class sws_t {
public:
static std::optional<sws_t> make(int in_width, int in_height, int out_width, int out_heigth, gl::tex_t &&tex);
static std::optional<sws_t> 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);
// 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 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 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;
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;
// Store latest cursor for load_vram
std::uint64_t serial;
};
bool fail();
} // namespace egl
#endif

1273
src/platform/linux/input.cpp Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

302
src/platform/linux/misc.cpp Normal file
View File

@@ -0,0 +1,302 @@
#include <arpa/inet.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <pwd.h>
#include <unistd.h>
#include <fstream>
#include "graphics.h"
#include "misc.h"
#include "vaapi.h"
#include "src/main.h"
#include "src/platform/common.h"
#ifdef __GNUC__
#define SUNSHINE_GNUC_EXTENSION __extension__
#else
#define SUNSHINE_GNUC_EXTENSION
#endif
using namespace std::literals;
namespace fs = std::filesystem;
window_system_e window_system;
namespace dyn {
void *handle(const std::vector<const char *> &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<std::tuple<apiproc *, const char *>> &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<ifaddrs, freeifaddrs>;
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<std::uint16_t, std::string> 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;
}
}
}
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
return "00:00:00:00:00:00"s;
}
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<source::MAX_FLAGS> sources;
#ifdef SUNSHINE_BUILD_CUDA
std::vector<std::string> nvfbc_display_names();
std::shared_ptr<display_t> nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
bool verify_nvfbc() {
return !nvfbc_display_names().empty();
}
#endif
#ifdef SUNSHINE_BUILD_WAYLAND
std::vector<std::string> wl_display_names();
std::shared_ptr<display_t> wl_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
bool verify_wl() {
return window_system == window_system_e::WAYLAND && !wl_display_names().empty();
}
#endif
#ifdef SUNSHINE_BUILD_DRM
std::vector<std::string> kms_display_names();
std::shared_ptr<display_t> kms_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
bool verify_kms() {
return !kms_display_names().empty();
}
#endif
#ifdef SUNSHINE_BUILD_X11
std::vector<std::string> x11_display_names();
std::shared_ptr<display_t> x11_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate);
bool verify_x11() {
return window_system == window_system_e::X11 && !x11_display_names().empty();
}
#endif
std::vector<std::string> 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_t> display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
#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, framerate);
}
#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, framerate);
}
#endif
#ifdef SUNSHINE_BUILD_DRM
if(sources[source::KMS]) {
BOOST_LOG(info) << "Screencasting with KMS"sv;
return kms_display(hwdevice_type, display_name, framerate);
}
#endif
#ifdef SUNSHINE_BUILD_X11
if(sources[source::X11]) {
BOOST_LOG(info) << "Screencasting with X11"sv;
return x11_display(hwdevice_type, display_name, framerate);
}
#endif
return nullptr;
}
std::unique_ptr<deinit_t> 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;
}
#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;
}
window_system = window_system_e::X11;
}
#endif
#ifdef SUNSHINE_BUILD_CUDA
if(verify_nvfbc()) {
sources[source::NVFBC] = true;
}
#endif
#ifdef SUNSHINE_BUILD_WAYLAND
if(verify_wl()) {
sources[source::WAYLAND] = true;
}
#endif
#ifdef SUNSHINE_BUILD_DRM
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;
}
#endif
#ifdef SUNSHINE_BUILD_X11
if(verify_x11()) {
sources[source::X11] = true;
}
#endif
if(sources.none()) {
return nullptr;
}
if(!gladLoaderLoadEGL(EGL_NO_DISPLAY) || !eglGetPlatformDisplay) {
BOOST_LOG(warning) << "Couldn't load EGL library"sv;
}
return std::make_unique<deinit_t>();
}
} // namespace platf

31
src/platform/linux/misc.h Normal file
View File

@@ -0,0 +1,31 @@
#ifndef SUNSHINE_PLATFORM_MISC_H
#define SUNSHINE_PLATFORM_MISC_H
#include <unistd.h>
#include <vector>
#include "src/utility.h"
KITTY_USING_MOVE_T(file_t, int, -1, {
if(el >= 0) {
close(el);
}
});
enum class window_system_e {
NONE,
X11,
WAYLAND,
};
extern window_system_e window_system;
namespace dyn {
typedef void (*apiproc)(void);
int load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict = true);
void *handle(const std::vector<const char *> &libs);
} // namespace dyn
#endif

View File

@@ -0,0 +1,429 @@
// adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html
#include <thread>
#include "misc.h"
#include "src/main.h"
#include "src/nvhttp.h"
#include "src/platform/common.h"
#include "src/utility.h"
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 */
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_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_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<std::tuple<dyn::apiproc *, const char *>> 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;
}
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<std::tuple<dyn::apiproc *, const char *>> 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<class T>
void free(T *p) {
avahi::free(p);
}
template<class T>
using ptr_t = util::safe_ptr<T, free<T>>;
using client_t = util::dyn_safe_ptr<avahi::Client, &avahi::client_free>;
using poll_t = util::dyn_safe_ptr<avahi::SimplePoll, &avahi::simple_poll_free>;
avahi::EntryGroup *group = nullptr;
poll_t poll;
client_t client;
ptr_t<char> 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:;
}
}
void create_services(avahi::Client *c) {
int ret;
auto fg = util::fail_guard([]() {
avahi::simple_poll_quit(poll.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;
}
}
if(avahi::entry_group_is_empty(group)) {
BOOST_LOG(info) << "Adding avahi service "sv << name.get();
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);
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;
}
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:;
}
}
class deinit_t : public ::platf::deinit_t {
public:
std::thread poll_thread;
deinit_t(std::thread poll_thread) : poll_thread { std::move(poll_thread) } {}
~deinit_t() override {
if(avahi::simple_poll_quit && poll) {
avahi::simple_poll_quit(poll.get());
}
if(poll_thread.joinable()) {
poll_thread.join();
}
}
};
[[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<deinit_t>(std::thread { avahi::simple_poll_loop, poll.get() });
}
} // namespace platf::publish

View File

@@ -0,0 +1,651 @@
#include <sstream>
#include <string>
#include <fcntl.h>
extern "C" {
#include <libavcodec/avcodec.h>
}
#include "graphics.h"
#include "misc.h"
#include "src/config.h"
#include "src/main.h"
#include "src/platform/common.h"
#include "src/utility.h"
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;
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;
uint32_t width;
uint32_t height;
// 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;
/*
* 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];
// 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;
// references objects --> DRMPRIMESurfaceDescriptor.objects[object_index[0]]
uint32_t object_index[4];
// Offset within the object of each plane.
uint32_t offset[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,
// 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 */
/*
* 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
* available with this entrypoint (e.g. interlace, MBAFF) and the
* 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. */
/**
* \brief FEI
*
* The purpose of FEI (Flexible Encoding Infrastructure) is to allow applications to
* have more controls and trade off quality for speed with their own IPs.
* The application can optionally provide input to ENC for extra encode control
* and get the output from ENC. Application can chose to modify the ENC
* output/PAK input during encoding, but the performance impact is significant.
*
* On top of the existing buffers for normal encode, there will be
* one extra input buffer (VAEncMiscParameterFEIFrameControl) and
* three extra output buffers (VAEncFEIMVBufferType, VAEncFEIMBModeBufferType
* and VAEncFEIDistortionBufferType) for FEI entry function.
* If separate PAK is set, two extra input buffers
* (VAEncFEIMVBufferType, VAEncFEIMBModeBufferType) are needed for PAK input.
**/
FEI = 11,
/**
* \brief Stats
*
* A pre-processing function for getting some statistics and motion vectors is added,
* and some extra controls for Encode pipeline are provided. The application can
* optionally call the statistics function to get motion vectors and statistics like
* variances, distortions before calling Encode function via this entry point.
*
* Checking whether Statistics is supported can be performed with vaQueryConfigEntrypoints().
* If Statistics entry point is supported, then the list of returned entry-points will
* include #Stats. Supported pixel format, maximum resolution and statistics
* specific attributes can be obtained via normal attribute query. One input buffer
* (VAStatsStatisticsParameterBufferType) and one or two output buffers
* (VAStatsStatisticsBufferType, VAStatsStatisticsBottomFieldBufferType (for interlace only)
* and VAStatsMVBufferType) are needed for this entry point.
**/
Stats = 12,
/**
* \brief ProtectedTEEComm
*
* A function for communicating with TEE (Trusted Execution Environment).
**/
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<void, VAStatus, &terminate>;
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<std::tuple<dyn::apiproc *, const char *>> 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;
}
funcs_loaded = true;
return 0;
}
int init() {
if(init_main_va()) {
return -1;
}
static void *handle { nullptr };
static bool funcs_loaded = false;
if(funcs_loaded) return 0;
if(!handle) {
handle = dyn::handle({ "libva-drm.so.2", "libva-drm.so" });
if(!handle) {
return -1;
}
}
std::vector<std::tuple<dyn::apiproc *, const char *>> 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) override {
this->hwframe.reset(frame);
this->frame = frame;
if(av_hwframe_get_buffer(frame->hw_frames_ctx, frame, 0)) {
BOOST_LOG(error) << "Couldn't get hwframe for VAAPI"sv;
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<file_t, egl::nv12_img_t::num_fds> 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;
}
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range) override {
sws.set_colorspace(colorspace, color_range);
}
va::display_t::pointer va_display;
file_t file;
frame_t hwframe;
gbm::gbm_t gbm;
egl::display_t display;
egl::ctx_t ctx;
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;
/**
* VAAPI connection details.
*
* Allocated as AVHWDeviceContext.hwctx
*/
typedef struct AVVAAPIDeviceContext {
/**
* The VADisplay handle, to be filled by the user.
*/
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;
static void __log(void *level, const char *msg) {
BOOST_LOG(*(boost::log::sources::severity_logger<int> *)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;
}
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;
}
static bool query(display_t::pointer display, profile_e profile) {
std::vector<entry_e> 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<platf::hwdevice_t> make_hwdevice(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram) {
if(vram) {
auto egl = std::make_shared<va::va_vram_t>();
if(egl->init(width, height, std::move(card), offset_x, offset_y)) {
return nullptr;
}
return egl;
}
else {
auto egl = std::make_shared<va::va_ram_t>();
if(egl->init(width, height, std::move(card))) {
return nullptr;
}
return egl;
}
}
std::shared_ptr<platf::hwdevice_t> 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<platf::hwdevice_t> make_hwdevice(int width, int height, bool vram) {
return make_hwdevice(width, height, 0, 0, vram);
}
} // namespace va

View File

@@ -0,0 +1,27 @@
#ifndef SUNSHINE_VAAPI_H
#define SUNSHINE_VAAPI_H
#include "misc.h"
#include "src/platform/common.h"
namespace egl {
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<platf::hwdevice_t> make_hwdevice(int width, int height, bool vram);
std::shared_ptr<platf::hwdevice_t> make_hwdevice(int width, int height, int offset_x, int offset_y, bool vram);
std::shared_ptr<platf::hwdevice_t> 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);
int init();
} // namespace va
#endif

View File

@@ -0,0 +1,268 @@
#include <wayland-client.h>
#include <wayland-util.h>
#include <cstdlib>
#include "graphics.h"
#include "src/main.h"
#include "src/platform/common.h"
#include "src/round_robin.h"
#include "src/utility.h"
#include "wayland.h"
extern const wl_interface wl_output_interface;
using namespace std::literals;
// Disable warning for converting incompatible functions
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
#pragma GCC diagnostic ignored "-Wpmf-conversions"
namespace wl {
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;
}
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 } {}
inline void monitor_t::xdg_name(zxdg_output_v1 *, const char *name) {
this->name = name;
BOOST_LOG(info) << "Name: "sv << this->name;
}
void monitor_t::xdg_description(zxdg_output_v1 *, const char *description) {
this->description = 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;
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;
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::listen(zxdg_output_manager_v1 *output_manager) {
auto xdg_output = zxdg_output_manager_v1_get_xdg_output(output_manager, output);
#define CLASS_CALL(x, y) x = (decltype(x))&y
CLASS_CALL(listener.name, monitor_t::xdg_name);
CLASS_CALL(listener.logical_size, monitor_t::xdg_size);
CLASS_CALL(listener.logical_position, monitor_t::xdg_position);
CLASS_CALL(listener.done, monitor_t::xdg_done);
CLASS_CALL(listener.description, monitor_t::xdg_description);
#undef CLASS_CALL
zxdg_output_v1_add_listener(xdg_output, &listener, this);
}
interface_t::interface_t() noexcept
: output_manager { nullptr }, listener {
(decltype(wl_registry_listener::global))&interface_t::add_interface,
(decltype(wl_registry_listener::global_remove))&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<monitor_t>(
(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;
}
}
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 {
(decltype(zwlr_export_dmabuf_frame_v1_listener::frame))&dmabuf_t::frame,
(decltype(zwlr_export_dmabuf_frame_v1_listener::object))&dmabuf_t::object,
(decltype(zwlr_export_dmabuf_frame_v1_listener::ready))&dmabuf_t::ready,
(decltype(zwlr_export_dmabuf_frame_v1_listener::cancel))&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();
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;
}
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,
zwlr_export_dmabuf_frame_v1_cancel_reason 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;
}
}
}
frame_t::frame_t() {
// File descriptors aren't open
std::fill_n(sd.fds, 4, -1);
};
std::vector<std::unique_ptr<monitor_t>> 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

View File

@@ -0,0 +1,216 @@
#ifndef SUNSHINE_WAYLAND_H
#define SUNSHINE_WAYLAND_H
#include <bitset>
#ifdef SUNSHINE_BUILD_WAYLAND
#include <wlr-export-dmabuf-unstable-v1.h>
#include <xdg-output-unstable-v1.h>
#endif
#include "graphics.h"
/**
* The classes defined in this macro block should only be used by
* cpp files whose compilation depends on SUNSHINE_BUILD_WAYLAND
*/
#ifdef SUNSHINE_BUILD_WAYLAND
namespace wl {
using display_internal_t = util::safe_ptr<wl_display, wl_display_disconnect>;
class frame_t {
public:
frame_t();
egl::surface_descriptor_t sd;
void destroy();
};
class dmabuf_t {
public:
enum status_e {
WAITING,
READY,
REINIT,
};
dmabuf_t(dmabuf_t &&) = delete;
dmabuf_t(const dmabuf_t &) = delete;
dmabuf_t &operator=(const dmabuf_t &) = delete;
dmabuf_t &operator=(dmabuf_t &&) = delete;
dmabuf_t();
void listen(zwlr_export_dmabuf_manager_v1 *dmabuf_manager, wl_output *output, bool blend_cursor = false);
~dmabuf_t();
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 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 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 cancel(
zwlr_export_dmabuf_frame_v1 *frame,
zwlr_export_dmabuf_frame_v1_cancel_reason reason);
inline frame_t *get_next_frame() {
return current_frame == &frames[0] ? &frames[1] : &frames[0];
}
status_e status;
std::array<frame_t, 2> frames;
frame_t *current_frame;
zwlr_export_dmabuf_frame_v1_listener listener;
};
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;
};
public:
enum interface_e {
XDG_OUTPUT,
WLR_EXPORT_DMABUF,
MAX_INTERFACES,
};
interface_t(interface_t &&) = delete;
interface_t(const interface_t &) = delete;
interface_t &operator=(const interface_t &) = delete;
interface_t &operator=(interface_t &&) = delete;
interface_t() noexcept;
void listen(wl_registry *registry);
std::vector<std::unique_ptr<monitor_t>> monitors;
zwlr_export_dmabuf_manager_v1 *dmabuf_manager;
zxdg_output_manager_v1 *output_manager;
bool operator[](interface_e bit) const {
return interface[bit];
}
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);
std::bitset<MAX_INTERFACES> 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);
// Roundtrip with Wayland connection
void roundtrip();
// 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();
}
private:
display_internal_t display_internal;
};
std::vector<std::unique_ptr<monitor_t>> monitors(const char *display_name = nullptr);
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;
monitor_t &operator=(const monitor_t &) = delete;
monitor_t &operator=(monitor_t &&) = delete;
monitor_t(wl_output *output);
void listen(zxdg_output_manager_v1 *output_manager);
wl_output *output;
std::string name;
std::string description;
platf::touch_port_t viewport;
};
inline std::vector<std::unique_ptr<monitor_t>> monitors(const char *display_name = nullptr) { return {}; }
inline int init() { return -1; }
} // namespace wl
#endif
#endif

View File

@@ -0,0 +1,369 @@
#include "src/platform/common.h"
#include "src/main.h"
#include "vaapi.h"
#include "wayland.h"
using namespace std::literals;
namespace wl {
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, int framerate) {
delay = std::chrono::nanoseconds { 1s } / framerate;
mem_type = hwdevice_type;
if(display.init()) {
return -1;
}
interface.listen(display.registry());
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();
}
}
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;
}
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;
}
} while(dmabuf.status == dmabuf_t::WAITING);
auto current_frame = dmabuf.current_frame;
if(
dmabuf.status == dmabuf_t::REINIT ||
current_frame->sd.width != width ||
current_frame->sd.height != height) {
return platf::capture_e::reinit;
}
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<platf::img_t> 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:
continue;
case platf::capture_e::ok:
img = snapshot_cb(img);
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 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]);
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, int framerate) {
if(wlr_t::init(hwdevice_type, display_name, framerate)) {
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<platf::hwdevice_t> 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<platf::hwdevice_t>();
}
std::shared_ptr<platf::img_t> alloc_img() override {
auto img = std::make_shared<img_t>();
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<platf::img_t> 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:
continue;
case platf::capture_e::ok:
img = snapshot_cb(img);
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;
}
std::shared_ptr<platf::img_t> alloc_img() override {
auto img = std::make_shared<egl::img_descriptor_t>();
img->sequence = 0;
img->serial = std::numeric_limits<decltype(img->serial)>::max();
img->data = nullptr;
// File descriptors aren't open
std::fill_n(img->sd.fds, 4, -1);
return img;
}
std::shared_ptr<platf::hwdevice_t> 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<platf::hwdevice_t>();
}
int dummy_img(platf::img_t *img) override {
return snapshot(img, 1000ms, false) != platf::capture_e::ok;
}
std::uint64_t sequence {};
};
} // namespace wl
namespace platf {
std::shared_ptr<display_t> wl_display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
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<wl::wlr_vram_t>();
if(wlr->init(hwdevice_type, display_name, framerate)) {
return nullptr;
}
return wlr;
}
auto wlr = std::make_shared<wl::wlr_ram_t>();
if(wlr->init(hwdevice_type, display_name, framerate)) {
return nullptr;
}
return wlr;
}
std::vector<std::string> wl_display_names() {
std::vector<std::string> 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

View File

@@ -0,0 +1,837 @@
//
// Created by loki on 6/21/19.
//
#include "src/platform/common.h"
#include <fstream>
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xfixes.h>
#include <X11/extensions/Xrandr.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <xcb/shm.h>
#include <xcb/xfixes.h>
#include "src/config.h"
#include "src/main.h"
#include "src/task_pool.h"
#include "cuda.h"
#include "graphics.h"
#include "misc.h"
#include "vaapi.h"
#include "x11grab.h"
using namespace std::literals;
namespace platf {
int load_xcb();
int load_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));
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<std::tuple<dyn::apiproc *, const char *>> 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));
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<std::tuple<dyn::apiproc *, const char *>> funcs {
{ (dyn::apiproc *)&GetCursorImage, "XFixesGetCursorImage" },
};
if(dyn::load(handle, funcs)) {
return -1;
}
funcs_loaded = true;
return 0;
}
} // namespace fix
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<std::tuple<dyn::apiproc *, const char *>> 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<std::tuple<dyn::apiproc *, const char *>> 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<std::tuple<dyn::apiproc *, const char *>> 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<xcb_connection_t, &xcb::disconnect>;
using xcb_img_t = util::c_ptr<xcb_shm_get_image_reply_t>;
using ximg_t = util::safe_ptr<XImage, freeImage>;
using xcursor_t = util::safe_ptr<XFixesCursorImage, freeX>;
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<uint16_t>(overlay->height, std::max(0, screen_height - overlay->y));
auto delta_width = std::min<uint16_t>(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, int framerate) {
if(!xdisplay) {
BOOST_LOG(error) << "Could not open X11 display"sv;
return -1;
}
delay = std::chrono::nanoseconds { 1s } / 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 {
width = xattr.width;
height = xattr.height;
}
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_t> 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:
std::this_thread::sleep_for(1ms);
continue;
case platf::capture_e::ok:
img = snapshot_cb(img);
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) {
refresh();
//The whole X server changed, so we gotta 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) };
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<img_t> alloc_img() override {
return std::make_shared<x11_img_t>();
}
std::shared_ptr<hwdevice_t> 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<hwdevice_t>();
}
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;
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_t> 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:
std::this_thread::sleep_for(1ms);
continue;
case platf::capture_e::ok:
img = snapshot_cb(img);
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 gotta 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<img_t> alloc_img() override {
auto img = std::make_shared<shm_img_t>();
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, int framerate) {
if(x11_attr_t::init(display_name, framerate)) {
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<display_t> x11_display(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
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<shm_attr_t>(hwdevice_type);
auto status = shm_disp->init(display_name, framerate);
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<x11_attr_t>(hwdevice_type);
if(x11_disp->init(display_name, framerate)) {
return nullptr;
}
return x11_disp;
}
std::vector<std::string> 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<std::string> 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> 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

View File

@@ -0,0 +1,60 @@
#ifndef SUNSHINE_X11_GRAB
#define SUNSHINE_X11_GRAB
#include <optional>
#include "src/platform/common.h"
#include "src/utility.h"
// X11 Display
extern "C" struct _XDisplay;
namespace egl {
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);
using cursor_ctx_t = util::safe_ptr<cursor_ctx_raw_t, freeCursorCtx>;
using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>;
class cursor_t {
public:
static std::optional<cursor_t> make();
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);
cursor_ctx_t ctx;
};
xdisplay_t make_display();
#else
// It's never something different from nullptr
util::safe_ptr<_XDisplay, std::default_delete<_XDisplay>>;
class cursor_t {
public:
static std::optional<cursor_t> make() { return std::nullopt; }
void capture(egl::cursor_t &) {}
void blend(img_t &, int, int) {}
};
xdisplay_t make_display() { return nullptr; }
#endif
} // namespace platf::x11
#endif

View File

@@ -0,0 +1,26 @@
#ifndef SUNSHINE_PLATFORM_AV_AUDIO_H
#define SUNSHINE_PLATFORM_AV_AUDIO_H
#import <AVFoundation/AVFoundation.h>
#include "third-party/TPCircularBuffer/TPCircularBuffer.h"
#define kBufferLength 2048
@interface AVAudio : NSObject <AVCaptureAudioDataOutputSampleBufferDelegate> {
@public
TPCircularBuffer audioSampleBuffer;
}
@property(nonatomic, assign) AVCaptureSession *audioCaptureSession;
@property(nonatomic, assign) AVCaptureConnection *audioConnection;
@property(nonatomic, assign) NSCondition *samplesArrivedSignal;
+ (NSArray *)microphoneNames;
+ (AVCaptureDevice *)findMicrophone:(NSString *)name;
- (int)setupMicrophone:(AVCaptureDevice *)device sampleRate:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels;
@end
#endif //SUNSHINE_PLATFORM_AV_AUDIO_H

View File

@@ -0,0 +1,120 @@
#import "av_audio.h"
@implementation AVAudio
+ (NSArray<AVCaptureDevice *> *)microphones {
AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInMicrophone,
AVCaptureDeviceTypeExternalUnknown]
mediaType:AVMediaTypeAudio
position:AVCaptureDevicePositionUnspecified];
return discoverySession.devices;
}
+ (NSArray<NSString *> *)microphoneNames {
NSMutableArray *result = [[NSMutableArray alloc] init];
for(AVCaptureDevice *device in [AVAudio microphones]) {
[result addObject:[device localizedName]];
}
return result;
}
+ (AVCaptureDevice *)findMicrophone:(NSString *)name {
for(AVCaptureDevice *device in [AVAudio microphones]) {
if([[device localizedName] isEqualToString:name]) {
return device;
}
}
return nil;
}
- (void)dealloc {
// make sure we don't process any further samples
self.audioConnection = nil;
// make sure nothing gets stuck on this signal
[self.samplesArrivedSignal signal];
[self.samplesArrivedSignal release];
TPCircularBufferCleanup(&audioSampleBuffer);
[super dealloc];
}
- (int)setupMicrophone:(AVCaptureDevice *)device sampleRate:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels {
self.audioCaptureSession = [[AVCaptureSession alloc] init];
NSError *error;
AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
if(audioInput == nil) {
return -1;
}
if([self.audioCaptureSession canAddInput:audioInput]) {
[self.audioCaptureSession addInput:audioInput];
}
else {
[audioInput dealloc];
return -1;
}
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
}];
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]) {
[self.audioCaptureSession addOutput:audioOutput];
}
else {
[audioInput release];
[audioOutput release];
return -1;
}
self.audioConnection = [audioOutput connectionWithMediaType:AVMediaTypeAudio];
[self.audioCaptureSession startRunning];
[audioInput release];
[audioOutput release];
self.samplesArrivedSignal = [[NSCondition alloc] init];
TPCircularBufferInit(&self->audioSampleBuffer, kBufferLength * channels);
return 0;
}
- (void)captureOutput:(AVCaptureOutput *)output
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
if(connection == self.audioConnection) {
AudioBufferList audioBufferList;
CMBlockBufferRef blockBuffer;
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, NULL, &audioBufferList, sizeof(audioBufferList), NULL, NULL, 0, &blockBuffer);
//NSAssert(audioBufferList.mNumberBuffers == 1, @"Expected interlveaved PCM format but buffer contained %u streams", audioBufferList.mNumberBuffers);
// this is safe, because an interleaved PCM stream has exactly one buffer
// and we don't want to do sanity checks in a performance critical exec path
AudioBuffer audioBuffer = audioBufferList.mBuffers[0];
TPCircularBufferProduceBytes(&self->audioSampleBuffer, audioBuffer.mData, audioBuffer.mDataByteSize);
[self.samplesArrivedSignal signal];
}
}
@end

View File

@@ -0,0 +1,18 @@
#ifndef av_img_t_h
#define av_img_t_h
#include "src/platform/common.h"
#include <CoreMedia/CoreMedia.h>
#include <CoreVideo/CoreVideo.h>
namespace platf {
struct av_img_t : public img_t {
CVPixelBufferRef pixel_buffer = nullptr;
CMSampleBufferRef sample_buffer = nullptr;
~av_img_t();
};
} // namespace platf
#endif /* av_img_t_h */

View File

@@ -0,0 +1,43 @@
#ifndef SUNSHINE_PLATFORM_AV_VIDEO_H
#define SUNSHINE_PLATFORM_AV_VIDEO_H
#import <AVFoundation/AVFoundation.h>
struct CaptureSession {
AVCaptureVideoDataOutput *output;
NSCondition *captureStopped;
};
@interface AVVideo : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
#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;
typedef bool (^FrameCallbackBlock)(CMSampleBufferRef);
@property(nonatomic, assign) AVCaptureSession *session;
@property(nonatomic, assign) NSMapTable<AVCaptureConnection *, AVCaptureVideoDataOutput *> *videoOutputs;
@property(nonatomic, assign) NSMapTable<AVCaptureConnection *, FrameCallbackBlock> *captureCallbacks;
@property(nonatomic, assign) NSMapTable<AVCaptureConnection *, dispatch_semaphore_t> *captureSignals;
+ (NSArray<NSDictionary *> *)displayNames;
- (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate;
- (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight;
- (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback;
@end
#endif //SUNSHINE_PLATFORM_AV_VIDEO_H

View File

@@ -0,0 +1,184 @@
#import "av_video.h"
@implementation AVVideo
// XXX: Currently, this function only returns the screen IDs as names,
// which is not very helpful to the user. The API to retrieve names
// was deprecated with 10.9+.
// However, there is a solution with little external code that can be used:
// https://stackoverflow.com/questions/20025868/cgdisplayioserviceport-is-deprecated-in-os-x-10-9-how-to-replace
+ (NSArray<NSDictionary *> *)displayNames {
CGDirectDisplayID displays[kMaxDisplays];
uint32_t count;
if(CGGetActiveDisplayList(kMaxDisplays, displays, &count) != kCGErrorSuccess) {
return [NSArray array];
}
NSMutableArray *result = [NSMutableArray array];
for(uint32_t i = 0; i < count; i++) {
[result addObject:@{
@"id": [NSNumber numberWithUnsignedInt:displays[i]],
@"name": [NSString stringWithFormat:@"%d", displays[i]]
}];
}
return [NSArray arrayWithArray:result];
}
- (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate {
self = [super init];
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.minFrameDuration = CMTimeMake(1, frameRate);
self.session = [[AVCaptureSession alloc] init];
self.videoOutputs = [[NSMapTable alloc] init];
self.captureCallbacks = [[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]) {
[self.session addInput:screenInput];
}
else {
[screenInput release];
return nil;
}
[self.session startRunning];
return self;
}
- (void)dealloc {
[self.videoOutputs release];
[self.captureCallbacks release];
[self.captureSignals release];
[self.session stopRunning];
[super dealloc];
}
- (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight {
CGImageRef screenshot = CGDisplayCreateImage(self.displayID);
self.frameWidth = frameWidth;
self.frameHeight = 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;
self.paddingBottom = 0;
}
else {
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;
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;
int adjust_bottom = delta_height - adjust_top;
self.paddingTop += adjust_top;
self.paddingBottom += adjust_bottom;
}
CFRelease(screenshot);
}
- (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback {
@synchronized(self) {
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]
}];
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]) {
[self.session addOutput:videoOutput];
}
else {
[videoOutput release];
return nil;
}
AVCaptureConnection *videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo];
dispatch_semaphore_t signal = dispatch_semaphore_create(0);
[self.videoOutputs setObject:videoOutput forKey:videoConnection];
[self.captureCallbacks setObject:frameCallback forKey:videoConnection];
[self.captureSignals setObject:signal forKey:videoConnection];
[self.session startRunning];
return signal;
}
}
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
FrameCallbackBlock callback = [self.captureCallbacks objectForKey:connection];
if(callback != nil) {
if(!callback(sampleBuffer)) {
@synchronized(self) {
[self.session stopRunning];
[self.captureCallbacks removeObjectForKey:connection];
[self.session removeOutput:[self.videoOutputs objectForKey:connection]];
[self.videoOutputs removeObjectForKey:connection];
dispatch_semaphore_signal([self.captureSignals objectForKey:connection]);
[self.captureSignals removeObjectForKey:connection];
[self.session startRunning];
}
}
}
}
@end

View File

@@ -0,0 +1,196 @@
#include "src/platform/common.h"
#include "src/platform/macos/av_img_t.h"
#include "src/platform/macos/av_video.h"
#include "src/platform/macos/nv12_zero_device.h"
#include "src/config.h"
#include "src/main.h"
namespace fs = std::filesystem;
namespace platf {
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_t> 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<av_img_t>(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);
return img_next != nullptr;
}];
dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
return capture_e::ok;
}
std::shared_ptr<img_t> alloc_img() override {
return std::make_shared<av_img_t>();
}
std::shared_ptr<hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) override {
if(pix_fmt == pix_fmt_e::yuv420p) {
av_capture.pixelFormat = kCVPixelFormatType_32BGRA;
return std::make_shared<hwdevice_t>();
}
else if(pix_fmt == pix_fmt_e::nv12) {
auto device = std::make_shared<nv12_zero_device>();
device->init(static_cast<void *>(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<AVVideo *>(display) setFrameWidth:width frameHeight:height];
}
static void setPixelFormat(void *display, OSType pixelFormat) {
static_cast<AVVideo *>(display).pixelFormat = pixelFormat;
}
};
std::shared_ptr<display_t> display(platf::mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
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<av_display_t>();
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];
}
}
}
display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate: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;
}
std::vector<std::string> display_names(mem_type_e hwdevice_type) {
__block std::vector<std::string> 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;
}
}

View File

@@ -0,0 +1,465 @@
#import <Carbon/Carbon.h>
#include <mach/mach.h>
#include <mach/mach_time.h>
#include "src/main.h"
#include "src/platform/common.h"
#include "src/utility.h"
// Delay for a double click
// FIXME: we probably want to make this configurable
#define MULTICLICK_DELAY_NS 500000000
namespace platf {
using namespace std::literals;
struct macos_input_t {
public:
CGDirectDisplayID display;
CGFloat displayScaling;
CGEventSourceRef source;
// 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
};
// 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;
}
// clang-format off
const KeyCodeMap kKeyCodesMap[] = {
{ 0x08 /* VKEY_BACK */, kVK_Delete },
{ 0x09 /* VKEY_TAB */, kVK_Tab },
{ 0x0A /* VKEY_BACKTAB */, 0x21E4 },
{ 0x0C /* VKEY_CLEAR */, kVK_ANSI_KeypadClear },
{ 0x0D /* VKEY_RETURN */, kVK_Return },
{ 0x10 /* VKEY_SHIFT */, kVK_Shift },
{ 0x11 /* VKEY_CONTROL */, kVK_Control },
{ 0x12 /* VKEY_MENU */, kVK_Option },
{ 0x13 /* VKEY_PAUSE */, -1 },
{ 0x14 /* VKEY_CAPITAL */, kVK_CapsLock },
{ 0x15 /* VKEY_KANA */, kVK_JIS_Kana },
{ 0x15 /* VKEY_HANGUL */, -1 },
{ 0x17 /* VKEY_JUNJA */, -1 },
{ 0x18 /* VKEY_FINAL */, -1 },
{ 0x19 /* VKEY_HANJA */, -1 },
{ 0x19 /* VKEY_KANJI */, -1 },
{ 0x1B /* VKEY_ESCAPE */, kVK_Escape },
{ 0x1C /* VKEY_CONVERT */, -1 },
{ 0x1D /* VKEY_NONCONVERT */, -1 },
{ 0x1E /* VKEY_ACCEPT */, -1 },
{ 0x1F /* VKEY_MODECHANGE */, -1 },
{ 0x20 /* VKEY_SPACE */, kVK_Space },
{ 0x21 /* VKEY_PRIOR */, kVK_PageUp },
{ 0x22 /* VKEY_NEXT */, kVK_PageDown },
{ 0x23 /* VKEY_END */, kVK_End },
{ 0x24 /* VKEY_HOME */, kVK_Home },
{ 0x25 /* VKEY_LEFT */, kVK_LeftArrow },
{ 0x26 /* VKEY_UP */, kVK_UpArrow },
{ 0x27 /* VKEY_RIGHT */, kVK_RightArrow },
{ 0x28 /* VKEY_DOWN */, kVK_DownArrow },
{ 0x29 /* VKEY_SELECT */, -1 },
{ 0x2A /* VKEY_PRINT */, -1 },
{ 0x2B /* VKEY_EXECUTE */, -1 },
{ 0x2C /* VKEY_SNAPSHOT */, -1 },
{ 0x2D /* VKEY_INSERT */, kVK_Help },
{ 0x2E /* VKEY_DELETE */, kVK_ForwardDelete },
{ 0x2F /* VKEY_HELP */, kVK_Help },
{ 0x30 /* VKEY_0 */, kVK_ANSI_0 },
{ 0x31 /* VKEY_1 */, kVK_ANSI_1 },
{ 0x32 /* VKEY_2 */, kVK_ANSI_2 },
{ 0x33 /* VKEY_3 */, kVK_ANSI_3 },
{ 0x34 /* VKEY_4 */, kVK_ANSI_4 },
{ 0x35 /* VKEY_5 */, kVK_ANSI_5 },
{ 0x36 /* VKEY_6 */, kVK_ANSI_6 },
{ 0x37 /* VKEY_7 */, kVK_ANSI_7 },
{ 0x38 /* VKEY_8 */, kVK_ANSI_8 },
{ 0x39 /* VKEY_9 */, kVK_ANSI_9 },
{ 0x41 /* VKEY_A */, kVK_ANSI_A },
{ 0x42 /* VKEY_B */, kVK_ANSI_B },
{ 0x43 /* VKEY_C */, kVK_ANSI_C },
{ 0x44 /* VKEY_D */, kVK_ANSI_D },
{ 0x45 /* VKEY_E */, kVK_ANSI_E },
{ 0x46 /* VKEY_F */, kVK_ANSI_F },
{ 0x47 /* VKEY_G */, kVK_ANSI_G },
{ 0x48 /* VKEY_H */, kVK_ANSI_H },
{ 0x49 /* VKEY_I */, kVK_ANSI_I },
{ 0x4A /* VKEY_J */, kVK_ANSI_J },
{ 0x4B /* VKEY_K */, kVK_ANSI_K },
{ 0x4C /* VKEY_L */, kVK_ANSI_L },
{ 0x4D /* VKEY_M */, kVK_ANSI_M },
{ 0x4E /* VKEY_N */, kVK_ANSI_N },
{ 0x4F /* VKEY_O */, kVK_ANSI_O },
{ 0x50 /* VKEY_P */, kVK_ANSI_P },
{ 0x51 /* VKEY_Q */, kVK_ANSI_Q },
{ 0x52 /* VKEY_R */, kVK_ANSI_R },
{ 0x53 /* VKEY_S */, kVK_ANSI_S },
{ 0x54 /* VKEY_T */, kVK_ANSI_T },
{ 0x55 /* VKEY_U */, kVK_ANSI_U },
{ 0x56 /* VKEY_V */, kVK_ANSI_V },
{ 0x57 /* VKEY_W */, kVK_ANSI_W },
{ 0x58 /* VKEY_X */, kVK_ANSI_X },
{ 0x59 /* VKEY_Y */, kVK_ANSI_Y },
{ 0x5A /* VKEY_Z */, kVK_ANSI_Z },
{ 0x5B /* VKEY_LWIN */, kVK_Command },
{ 0x5C /* VKEY_RWIN */, kVK_RightCommand },
{ 0x5D /* VKEY_APPS */, kVK_RightCommand },
{ 0x5F /* VKEY_SLEEP */, -1 },
{ 0x60 /* VKEY_NUMPAD0 */, kVK_ANSI_Keypad0 },
{ 0x61 /* VKEY_NUMPAD1 */, kVK_ANSI_Keypad1 },
{ 0x62 /* VKEY_NUMPAD2 */, kVK_ANSI_Keypad2 },
{ 0x63 /* VKEY_NUMPAD3 */, kVK_ANSI_Keypad3 },
{ 0x64 /* VKEY_NUMPAD4 */, kVK_ANSI_Keypad4 },
{ 0x65 /* VKEY_NUMPAD5 */, kVK_ANSI_Keypad5 },
{ 0x66 /* VKEY_NUMPAD6 */, kVK_ANSI_Keypad6 },
{ 0x67 /* VKEY_NUMPAD7 */, kVK_ANSI_Keypad7 },
{ 0x68 /* VKEY_NUMPAD8 */, kVK_ANSI_Keypad8 },
{ 0x69 /* VKEY_NUMPAD9 */, kVK_ANSI_Keypad9 },
{ 0x6A /* VKEY_MULTIPLY */, kVK_ANSI_KeypadMultiply },
{ 0x6B /* VKEY_ADD */, kVK_ANSI_KeypadPlus },
{ 0x6C /* VKEY_SEPARATOR */, -1 },
{ 0x6D /* VKEY_SUBTRACT */, kVK_ANSI_KeypadMinus },
{ 0x6E /* VKEY_DECIMAL */, kVK_ANSI_KeypadDecimal },
{ 0x6F /* VKEY_DIVIDE */, kVK_ANSI_KeypadDivide },
{ 0x70 /* VKEY_F1 */, kVK_F1 },
{ 0x71 /* VKEY_F2 */, kVK_F2 },
{ 0x72 /* VKEY_F3 */, kVK_F3 },
{ 0x73 /* VKEY_F4 */, kVK_F4 },
{ 0x74 /* VKEY_F5 */, kVK_F5 },
{ 0x75 /* VKEY_F6 */, kVK_F6 },
{ 0x76 /* VKEY_F7 */, kVK_F7 },
{ 0x77 /* VKEY_F8 */, kVK_F8 },
{ 0x78 /* VKEY_F9 */, kVK_F9 },
{ 0x79 /* VKEY_F10 */, kVK_F10 },
{ 0x7A /* VKEY_F11 */, kVK_F11 },
{ 0x7B /* VKEY_F12 */, kVK_F12 },
{ 0x7C /* VKEY_F13 */, kVK_F13 },
{ 0x7D /* VKEY_F14 */, kVK_F14 },
{ 0x7E /* VKEY_F15 */, kVK_F15 },
{ 0x7F /* VKEY_F16 */, kVK_F16 },
{ 0x80 /* VKEY_F17 */, kVK_F17 },
{ 0x81 /* VKEY_F18 */, kVK_F18 },
{ 0x82 /* VKEY_F19 */, kVK_F19 },
{ 0x83 /* VKEY_F20 */, kVK_F20 },
{ 0x84 /* VKEY_F21 */, -1 },
{ 0x85 /* VKEY_F22 */, -1 },
{ 0x86 /* VKEY_F23 */, -1 },
{ 0x87 /* VKEY_F24 */, -1 },
{ 0x90 /* VKEY_NUMLOCK */, -1 },
{ 0x91 /* VKEY_SCROLL */, -1 },
{ 0xA0 /* VKEY_LSHIFT */, kVK_Shift },
{ 0xA1 /* VKEY_RSHIFT */, kVK_RightShift },
{ 0xA2 /* VKEY_LCONTROL */, kVK_Control },
{ 0xA3 /* VKEY_RCONTROL */, kVK_RightControl },
{ 0xA4 /* VKEY_LMENU */, kVK_Option },
{ 0xA5 /* VKEY_RMENU */, kVK_RightOption },
{ 0xA6 /* VKEY_BROWSER_BACK */, -1 },
{ 0xA7 /* VKEY_BROWSER_FORWARD */, -1 },
{ 0xA8 /* VKEY_BROWSER_REFRESH */, -1 },
{ 0xA9 /* VKEY_BROWSER_STOP */, -1 },
{ 0xAA /* VKEY_BROWSER_SEARCH */, -1 },
{ 0xAB /* VKEY_BROWSER_FAVORITES */, -1 },
{ 0xAC /* VKEY_BROWSER_HOME */, -1 },
{ 0xAD /* VKEY_VOLUME_MUTE */, -1 },
{ 0xAE /* VKEY_VOLUME_DOWN */, -1 },
{ 0xAF /* VKEY_VOLUME_UP */, -1 },
{ 0xB0 /* VKEY_MEDIA_NEXT_TRACK */, -1 },
{ 0xB1 /* VKEY_MEDIA_PREV_TRACK */, -1 },
{ 0xB2 /* VKEY_MEDIA_STOP */, -1 },
{ 0xB3 /* VKEY_MEDIA_PLAY_PAUSE */, -1 },
{ 0xB4 /* VKEY_MEDIA_LAUNCH_MAIL */, -1 },
{ 0xB5 /* VKEY_MEDIA_LAUNCH_MEDIA_SELECT */, -1 },
{ 0xB6 /* VKEY_MEDIA_LAUNCH_APP1 */, -1 },
{ 0xB7 /* VKEY_MEDIA_LAUNCH_APP2 */, -1 },
{ 0xBA /* VKEY_OEM_1 */, kVK_ANSI_Semicolon },
{ 0xBB /* VKEY_OEM_PLUS */, kVK_ANSI_Equal },
{ 0xBC /* VKEY_OEM_COMMA */, kVK_ANSI_Comma },
{ 0xBD /* VKEY_OEM_MINUS */, kVK_ANSI_Minus },
{ 0xBE /* VKEY_OEM_PERIOD */, kVK_ANSI_Period },
{ 0xBF /* VKEY_OEM_2 */, kVK_ANSI_Slash },
{ 0xC0 /* VKEY_OEM_3 */, kVK_ANSI_Grave },
{ 0xDB /* VKEY_OEM_4 */, kVK_ANSI_LeftBracket },
{ 0xDC /* VKEY_OEM_5 */, kVK_ANSI_Backslash },
{ 0xDD /* VKEY_OEM_6 */, kVK_ANSI_RightBracket },
{ 0xDE /* VKEY_OEM_7 */, kVK_ANSI_Quote },
{ 0xDF /* VKEY_OEM_8 */, -1 },
{ 0xE2 /* VKEY_OEM_102 */, -1 },
{ 0xE5 /* VKEY_PROCESSKEY */, -1 },
{ 0xE7 /* VKEY_PACKET */, -1 },
{ 0xF6 /* VKEY_ATTN */, -1 },
{ 0xF7 /* VKEY_CRSEL */, -1 },
{ 0xF8 /* VKEY_EXSEL */, -1 },
{ 0xF9 /* VKEY_EREOF */, -1 },
{ 0xFA /* VKEY_PLAY */, -1 },
{ 0xFB /* VKEY_ZOOM */, -1 },
{ 0xFC /* VKEY_NONAME */, -1 },
{ 0xFD /* VKEY_PA1 */, -1 },
{ 0xFE /* VKEY_OEM_CLEAR */, kVK_ANSI_KeypadClear }
};
// clang-format on
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);
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);
}
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;
}
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();
}
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);
}
input_t input() {
input_t result { new macos_input_t() };
auto macos_input = (macos_input_t *)result.get();
// If we don't use the main display in the future, this has to be adapted
macos_input->display = CGMainDisplayID();
// 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->source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
macos_input->kb_event = CGEventCreate(macos_input->source);
macos_input->kb_flags = 0;
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;
BOOST_LOG(debug) << "Display "sv << macos_input->display << ", pixel dimention: " << CGDisplayPixelsWide(macos_input->display) << "x"sv << CGDisplayPixelsHigh(macos_input->display);
return result;
}
void freeInput(void *p) {
auto *input = (macos_input_t *)p;
CFRelease(input->source);
CFRelease(input->kb_event);
CFRelease(input->mouse_event);
delete input;
}
std::vector<std::string_view> &supported_gamepads() {
static std::vector<std::string_view> gamepads { ""sv };
return gamepads;
}
} // namespace platf

View File

@@ -0,0 +1,87 @@
#include "src/platform/common.h"
#include "src/platform/macos/av_audio.h"
#include "src/config.h"
#include "src/main.h"
namespace platf {
using namespace std::literals;
struct av_mic_t : public mic_t {
AVAudio *av_audio_capture;
~av_mic_t() {
[av_audio_capture release];
}
capture_e sample(std::vector<std::int16_t> &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);
}
const int16_t *sampleBuffer = (int16_t *)byteSampleBuffer;
std::vector<int16_t> 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;
}
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override {
auto mic = std::make_unique<av_mic_t>();
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];
}
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;
}
std::optional<sink_t> sink_info() override {
sink_t sink;
return sink;
}
};
std::unique_ptr<audio_control_t> audio_control() {
return std::make_unique<macos_audio_control_t>();
}
}

161
src/platform/macos/misc.cpp Normal file
View File

@@ -0,0 +1,161 @@
#include <arpa/inet.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <net/if_dl.h>
#include <pwd.h>
#include "misc.h"
#include "src/main.h"
#include "src/platform/common.h"
using namespace std::literals;
namespace fs = std::filesystem;
namespace platf {
std::unique_ptr<deinit_t> init() {
if(!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;
}
return std::make_unique<deinit_t>();
}
fs::path appdata() {
const char *homedir;
if((homedir = getenv("HOME")) == nullptr) {
homedir = getpwuid(geteuid())->pw_dir;
}
return fs::path { homedir } / ".config/sunshine"sv;
}
using ifaddr_t = util::safe_ptr<ifaddrs, freeifaddrs>;
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);
}
if(family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in *)ip_addr)->sin_addr, data,
INET_ADDRSTRLEN);
}
return std::string { data };
}
std::pair<std::uint16_t, std::string> 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)) {
BOOST_LOG(verbose) << "Looking for MAC of "sv << pos->ifa_name;
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];
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;
}
}
}
}
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
return "00:00:00:00:00:00"s;
}
} // namespace platf
namespace dyn {
void *handle(const std::vector<const char *> &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<std::tuple<apiproc *, const char *>> &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;
}
}
return err;
}
} // namespace dyn

16
src/platform/macos/misc.h Normal file
View File

@@ -0,0 +1,16 @@
#ifndef SUNSHINE_PLATFORM_MISC_H
#define SUNSHINE_PLATFORM_MISC_H
#include <vector>
#include <CoreGraphics/CoreGraphics.h>
namespace dyn {
typedef void (*apiproc)(void);
int load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict = true);
void *handle(const std::vector<const char *> &libs);
} // namespace dyn
#endif

View File

@@ -0,0 +1,82 @@
#include "src/platform/macos/nv12_zero_device.h"
#include "src/platform/macos/av_img_t.h"
#include "src/video.h"
extern "C" {
#include "libavutil/imgutils.h"
}
namespace platf {
void free_frame(AVFrame *frame) {
av_frame_free(&frame);
}
util::safe_ptr<AVFrame, free_frame> av_frame;
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;
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);
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
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(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;
}
int nv12_zero_device::set_frame(AVFrame *frame) {
this->frame = frame;
av_frame.reset(frame);
resolution_fn(this->display, frame->width, frame->height);
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

View File

@@ -0,0 +1,29 @@
#ifndef vtdevice_h
#define vtdevice_h
#include "src/platform/common.h"
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;
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<void(void *display, int width, int height)>;
resolution_fn_t resolution_fn;
using pixel_format_fn_t = std::function<void(void *display, int pixelFormat)>;
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);
void set_colorspace(std::uint32_t colorspace, std::uint32_t color_range);
};
} // namespace platf
#endif /* vtdevice_h */

View File

@@ -0,0 +1,429 @@
// adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html
#include <thread>
#include "misc.h"
#include "src/main.h"
#include "src/nvhttp.h"
#include "src/platform/common.h"
#include "src/utility.h"
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 */
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_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_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<std::tuple<dyn::apiproc *, const char *>> 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;
}
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<std::tuple<dyn::apiproc *, const char *>> 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<class T>
void free(T *p) {
avahi::free(p);
}
template<class T>
using ptr_t = util::safe_ptr<T, free<T>>;
using client_t = util::dyn_safe_ptr<avahi::Client, &avahi::client_free>;
using poll_t = util::dyn_safe_ptr<avahi::SimplePoll, &avahi::simple_poll_free>;
avahi::EntryGroup *group = nullptr;
poll_t poll;
client_t client;
ptr_t<char> 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:;
}
}
void create_services(avahi::Client *c) {
int ret;
auto fg = util::fail_guard([]() {
avahi::simple_poll_quit(poll.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;
}
}
if(avahi::entry_group_is_empty(group)) {
BOOST_LOG(info) << "Adding avahi service "sv << name.get();
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);
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;
}
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:;
}
}
class deinit_t : public ::platf::deinit_t {
public:
std::thread poll_thread;
deinit_t(std::thread poll_thread) : poll_thread { std::move(poll_thread) } {}
~deinit_t() override {
if(avahi::simple_poll_quit && poll) {
avahi::simple_poll_quit(poll.get());
}
if(poll_thread.joinable()) {
poll_thread.join();
}
}
};
[[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<deinit_t>(std::thread { avahi::simple_poll_loop, poll.get() });
}
}; // namespace platf::publish

View File

@@ -0,0 +1,164 @@
// ----------------------------------------------------------------------------
// PolicyConfig.h
// Undocumented COM-interface IPolicyConfig.
// Use for set default audio render endpoint
// @author EreTIk
// 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
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);
#endif
interface DECLSPEC_UUID("f8679f50-850a-41cf-9c72-430f290290c8") IPolicyConfig;
class DECLSPEC_UUID("870af99c-171d-4f9e-af0d-e63df40c2bc9") CPolicyConfigClient;
// ----------------------------------------------------------------------------
// class CPolicyConfigClient
// {870af99c-171d-4f9e-af0d-e63df40c2bc9}
//
// interface IPolicyConfig
// {f8679f50-850a-41cf-9c72-430f290290c8}
//
// Query interface:
// CComPtr<IPolicyConfig> PolicyConfig;
// PolicyConfig.CoCreateInstance(__uuidof(CPolicyConfigClient));
//
// @compatible: Windows 7 and Later
// ----------------------------------------------------------------------------
interface IPolicyConfig : public IUnknown {
public:
virtual HRESULT GetMixFormat(
PCWSTR,
WAVEFORMATEX **);
virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat(
PCWSTR,
INT,
WAVEFORMATEX **);
virtual HRESULT STDMETHODCALLTYPE ResetDeviceFormat(
PCWSTR);
virtual HRESULT STDMETHODCALLTYPE SetDeviceFormat(
PCWSTR,
WAVEFORMATEX *,
WAVEFORMATEX *);
virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod(
PCWSTR,
INT,
PINT64,
PINT64);
virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod(
PCWSTR,
PINT64);
virtual HRESULT STDMETHODCALLTYPE GetShareMode(
PCWSTR,
struct DeviceShareMode *);
virtual HRESULT STDMETHODCALLTYPE SetShareMode(
PCWSTR,
struct DeviceShareMode *);
virtual HRESULT STDMETHODCALLTYPE GetPropertyValue(
PCWSTR,
const PROPERTYKEY &,
PROPVARIANT *);
virtual HRESULT STDMETHODCALLTYPE SetPropertyValue(
PCWSTR,
const PROPERTYKEY &,
PROPVARIANT *);
virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint(
PCWSTR wszDeviceId,
ERole eRole);
virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility(
PCWSTR,
INT);
};
interface DECLSPEC_UUID("568b9108-44bf-40b4-9006-86afe5b5a620") IPolicyConfigVista;
class DECLSPEC_UUID("294935CE-F637-4E7C-A41B-AB255460B862") CPolicyConfigVistaClient;
// ----------------------------------------------------------------------------
// class CPolicyConfigVistaClient
// {294935CE-F637-4E7C-A41B-AB255460B862}
//
// interface IPolicyConfigVista
// {568b9108-44bf-40b4-9006-86afe5b5a620}
//
// Query interface:
// CComPtr<IPolicyConfigVista> PolicyConfig;
// PolicyConfig.CoCreateInstance(__uuidof(CPolicyConfigVistaClient));
//
// @compatible: Windows Vista and Later
// ----------------------------------------------------------------------------
interface IPolicyConfigVista : public IUnknown {
public:
virtual HRESULT GetMixFormat(
PCWSTR,
WAVEFORMATEX **); // not available on Windows 7, use method from IPolicyConfig
virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat(
PCWSTR,
INT,
WAVEFORMATEX **);
virtual HRESULT STDMETHODCALLTYPE SetDeviceFormat(
PCWSTR,
WAVEFORMATEX *,
WAVEFORMATEX *);
virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod(
PCWSTR,
INT,
PINT64,
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
virtual HRESULT STDMETHODCALLTYPE GetShareMode(
PCWSTR,
struct DeviceShareMode *); // not available on Windows 7, use method from IPolicyConfig
virtual HRESULT STDMETHODCALLTYPE SetShareMode(
PCWSTR,
struct DeviceShareMode *); // not available on Windows 7, use method from IPolicyConfig
virtual HRESULT STDMETHODCALLTYPE GetPropertyValue(
PCWSTR,
const PROPERTYKEY &,
PROPVARIANT *);
virtual HRESULT STDMETHODCALLTYPE SetPropertyValue(
PCWSTR,
const PROPERTYKEY &,
PROPVARIANT *);
virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint(
PCWSTR wszDeviceId,
ERole eRole);
virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility(
PCWSTR,
INT); // not available on Windows 7, use method from IPolicyConfig
};
// ----------------------------------------------------------------------------

View File

@@ -0,0 +1,988 @@
//
// Created by loki on 1/12/20.
//
#include <audioclient.h>
#include <mmdeviceapi.h>
#include <roapi.h>
#include <codecvt>
#include <synchapi.h>
#define INITGUID
#include <propkeydef.h>
#undef INITGUID
#include "src/config.h"
#include "src/main.h"
#include "src/platform/common.h"
// Must be the last included file
// clang-format off
#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_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);
using namespace std::literals;
namespace platf::audio {
constexpr auto SAMPLE_RATE = 48000;
template<class T>
void Release(T *p) {
p->Release();
}
template<class T>
void co_task_free(T *p) {
CoTaskMemFree((LPVOID)p);
}
using device_enum_t = util::safe_ptr<IMMDeviceEnumerator, Release<IMMDeviceEnumerator>>;
using device_t = util::safe_ptr<IMMDevice, Release<IMMDevice>>;
using collection_t = util::safe_ptr<IMMDeviceCollection, Release<IMMDeviceCollection>>;
using audio_client_t = util::safe_ptr<IAudioClient, Release<IAudioClient>>;
using audio_capture_t = util::safe_ptr<IAudioCaptureClient, Release<IAudioCaptureClient>>;
using wave_format_t = util::safe_ptr<WAVEFORMATEX, co_task_free<WAVEFORMATEX>>;
using wstring_t = util::safe_ptr<WCHAR, co_task_free<WCHAR>>;
using handle_t = util::safe_ptr_v2<void, BOOL, CloseHandle>;
using policy_t = util::safe_ptr<IPolicyConfig, Release<IPolicyConfig>>;
using prop_t = util::safe_ptr<IPropertyStore, Release<IPropertyStore>>;
class co_init_t : public deinit_t {
public:
co_init_t() {
CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY);
}
~co_init_t() override {
CoUninitialize();
}
};
class prop_var_t {
public:
prop_var_t() {
PropVariantInit(&prop);
}
~prop_var_t() {
PropVariantClear(&prop);
}
PROPVARIANT prop;
};
class audio_pipe_t {
public:
static constexpr auto stereo = 2;
static constexpr auto channels51 = 6;
static constexpr auto channels71 = 8;
using samples_t = std::vector<std::int16_t>;
using buf_t = util::buffer_t<std::int16_t>;
virtual void to_stereo(samples_t &out, const buf_t &in) = 0;
virtual void to_51(samples_t &out, const buf_t &in) = 0;
virtual void to_71(samples_t &out, const buf_t &in) = 0;
};
class mono_t : public audio_pipe_t {
public:
void to_stereo(samples_t &out, const buf_t &in) override {
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end;) {
*sample_out_p++ = *sample_in_pos * 7 / 10;
*sample_out_p++ = *sample_in_pos++ * 7 / 10;
}
}
void to_51(samples_t &out, const buf_t &in) override {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels51) {
int left = *sample_in_pos++;
auto fl = (left * 7 / 10);
sample_out_p[FRONT_LEFT] = fl;
sample_out_p[FRONT_RIGHT] = fl;
sample_out_p[FRONT_CENTER] = fl * 6;
sample_out_p[LOW_FREQUENCY] = fl / 10;
sample_out_p[BACK_LEFT] = left * 4 / 10;
sample_out_p[BACK_RIGHT] = left * 4 / 10;
}
}
void to_71(samples_t &out, const buf_t &in) override {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels71) {
int left = *sample_in_pos++;
auto fl = (left * 7 / 10);
sample_out_p[FRONT_LEFT] = fl;
sample_out_p[FRONT_RIGHT] = fl;
sample_out_p[FRONT_CENTER] = fl * 6;
sample_out_p[LOW_FREQUENCY] = fl / 10;
sample_out_p[BACK_LEFT] = left * 4 / 10;
sample_out_p[BACK_RIGHT] = left * 4 / 10;
sample_out_p[SIDE_LEFT] = left * 5 / 10;
sample_out_p[SIDE_RIGHT] = left * 5 / 10;
}
}
};
class stereo_t : public audio_pipe_t {
public:
void to_stereo(samples_t &out, const buf_t &in) override {
std::copy_n(std::begin(in), out.size(), std::begin(out));
}
void to_51(samples_t &out, const buf_t &in) override {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels51) {
int left = sample_in_pos[speaker::FRONT_LEFT];
int right = sample_in_pos[speaker::FRONT_RIGHT];
sample_in_pos += 2;
auto fl = (left * 7 / 10);
auto fr = (right * 7 / 10);
auto mix = (fl + fr) / 2;
sample_out_p[FRONT_LEFT] = fl;
sample_out_p[FRONT_RIGHT] = fr;
sample_out_p[FRONT_CENTER] = mix;
sample_out_p[LOW_FREQUENCY] = mix / 2;
sample_out_p[BACK_LEFT] = left * 4 / 10;
sample_out_p[BACK_RIGHT] = right * 4 / 10;
}
}
void to_71(samples_t &out, const buf_t &in) override {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels71) {
int left = sample_in_pos[speaker::FRONT_LEFT];
int right = sample_in_pos[speaker::FRONT_RIGHT];
sample_in_pos += 2;
auto fl = (left * 7 / 10);
auto fr = (right * 7 / 10);
auto mix = (fl + fr) / 2;
sample_out_p[FRONT_LEFT] = fl;
sample_out_p[FRONT_RIGHT] = fr;
sample_out_p[FRONT_CENTER] = mix;
sample_out_p[LOW_FREQUENCY] = mix / 2;
sample_out_p[BACK_LEFT] = left * 4 / 10;
sample_out_p[BACK_RIGHT] = right * 4 / 10;
sample_out_p[SIDE_LEFT] = left * 5 / 10;
sample_out_p[SIDE_RIGHT] = right * 5 / 10;
}
}
};
class surr51_t : public audio_pipe_t {
public:
void to_stereo(samples_t &out, const buf_t &in) {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += stereo) {
int left {}, right {};
left += sample_in_pos[FRONT_LEFT];
left += sample_in_pos[FRONT_CENTER] * 9 / 10;
left += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
left += sample_in_pos[BACK_LEFT] * 7 / 10;
left += sample_in_pos[BACK_RIGHT] * 3 / 10;
right += sample_in_pos[FRONT_RIGHT];
right += sample_in_pos[FRONT_CENTER] * 9 / 10;
right += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
right += sample_in_pos[BACK_LEFT] * 3 / 10;
right += sample_in_pos[BACK_RIGHT] * 7 / 10;
sample_out_p[0] = left;
sample_out_p[1] = right;
sample_in_pos += channels51;
}
}
void to_51(samples_t &out, const buf_t &in) override {
std::copy_n(std::begin(in), out.size(), std::begin(out));
}
void to_71(samples_t &out, const buf_t &in) override {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels71) {
int fl = sample_in_pos[FRONT_LEFT];
int fr = sample_in_pos[FRONT_RIGHT];
int bl = sample_in_pos[BACK_LEFT];
int br = sample_in_pos[BACK_RIGHT];
auto mix_l = (fl + bl) / 2;
auto mix_r = (bl + br) / 2;
sample_out_p[FRONT_LEFT] = fl;
sample_out_p[FRONT_RIGHT] = fr;
sample_out_p[FRONT_CENTER] = sample_in_pos[FRONT_CENTER];
sample_out_p[LOW_FREQUENCY] = sample_in_pos[LOW_FREQUENCY];
sample_out_p[BACK_LEFT] = bl;
sample_out_p[BACK_RIGHT] = br;
sample_out_p[SIDE_LEFT] = mix_l;
sample_out_p[SIDE_RIGHT] = mix_r;
sample_in_pos += channels51;
}
}
};
class surr71_t : public audio_pipe_t {
public:
void to_stereo(samples_t &out, const buf_t &in) {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += stereo) {
int left {}, right {};
left += sample_in_pos[FRONT_LEFT];
left += sample_in_pos[FRONT_CENTER] * 9 / 10;
left += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
left += sample_in_pos[BACK_LEFT] * 7 / 10;
left += sample_in_pos[BACK_RIGHT] * 3 / 10;
left += sample_in_pos[SIDE_LEFT];
right += sample_in_pos[FRONT_RIGHT];
right += sample_in_pos[FRONT_CENTER] * 9 / 10;
right += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
right += sample_in_pos[BACK_LEFT] * 3 / 10;
right += sample_in_pos[BACK_RIGHT] * 7 / 10;
right += sample_in_pos[SIDE_RIGHT];
sample_out_p[0] = left;
sample_out_p[1] = right;
sample_in_pos += channels71;
}
}
void to_51(samples_t &out, const buf_t &in) override {
using namespace speaker;
auto sample_in_pos = std::begin(in);
auto sample_end = std::begin(out) + out.size();
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels51) {
auto sl = (int)sample_out_p[SIDE_LEFT] * 3 / 10;
auto sr = (int)sample_out_p[SIDE_RIGHT] * 3 / 10;
sample_out_p[FRONT_LEFT] = sample_in_pos[FRONT_LEFT] + sl;
sample_out_p[FRONT_RIGHT] = sample_in_pos[FRONT_RIGHT] + sr;
sample_out_p[FRONT_CENTER] = sample_in_pos[FRONT_CENTER];
sample_out_p[LOW_FREQUENCY] = sample_in_pos[LOW_FREQUENCY];
sample_out_p[BACK_LEFT] = sample_in_pos[BACK_LEFT] + sl;
sample_out_p[BACK_RIGHT] = sample_in_pos[BACK_RIGHT] + sr;
sample_in_pos += channels71;
}
}
void to_71(samples_t &out, const buf_t &in) override {
std::copy_n(std::begin(in), out.size(), std::begin(out));
}
};
static std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
struct format_t {
enum type_e : int {
none,
mono,
stereo,
surr51,
surr71,
} type;
std::string_view name;
int channels;
int channel_mask;
} formats[] {
{
format_t::mono,
"Mono"sv,
1,
SPEAKER_FRONT_CENTER,
},
{
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,
SPEAKER_FRONT_LEFT |
SPEAKER_FRONT_RIGHT |
SPEAKER_FRONT_CENTER |
SPEAKER_LOW_FREQUENCY |
SPEAKER_SIDE_LEFT |
SPEAKER_SIDE_RIGHT,
};
void set_wave_format(audio::wave_format_t &wave_format, const format_t &format) {
wave_format->nChannels = format.channels;
wave_format->nBlockAlign = wave_format->nChannels * wave_format->wBitsPerSample / 8;
wave_format->nAvgBytesPerSec = wave_format->nSamplesPerSec * wave_format->nBlockAlign;
if(wave_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
((PWAVEFORMATEXTENSIBLE)wave_format.get())->dwChannelMask = format.channel_mask;
}
}
int init_wave_format(audio::wave_format_t &wave_format, DWORD sample_rate) {
wave_format->wBitsPerSample = 16;
wave_format->nSamplesPerSec = sample_rate;
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();
if(IsEqualGUID(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, wave_ex->SubFormat)) {
wave_ex->Samples.wValidBitsPerSample = 16;
wave_ex->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
break;
}
BOOST_LOG(error) << "Unsupported Sub Format for WAVE_FORMAT_EXTENSIBLE: [0x"sv << util::hex(wave_ex->SubFormat).to_string_view() << ']';
}
default:
BOOST_LOG(error) << "Unsupported Wave Format: [0x"sv << util::hex(wave_format->wFormatTag).to_string_view() << ']';
return -1;
};
return 0;
}
audio_client_t make_audio_client(device_t &device, const format_t &format, int sample_rate) {
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;
}
wave_format_t wave_format;
status = audio_client->GetMixFormat(&wave_format);
if(FAILED(status)) {
BOOST_LOG(error) << "Couldn't acquire Wave Format [0x"sv << util::hex(status).to_string_view() << ']';
return nullptr;
}
if(init_wave_format(wave_format, sample_rate)) {
return nullptr;
}
set_wave_format(wave_format, format);
status = audio_client->Initialize(
AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
0, 0,
wave_format.get(),
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";
}
format_t::type_e validate_device(device_t &device, int sample_rate) {
for(const auto &format : formats) {
// Ensure WaveFromat is compatible
auto audio_client = make_audio_client(device, format, sample_rate);
BOOST_LOG(debug) << format.name << ": "sv << (!audio_client ? "unsupported"sv : "supported"sv);
if(audio_client) {
return format.type;
}
}
return format_t::none;
}
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<std::int16_t> &sample_out) override {
auto sample_size = sample_out.size() / channels_out * channels_in;
while(sample_buf_pos - std::begin(sample_buf) < sample_size) {
//FIXME: Use IAudioClient3 instead of IAudioClient, that would allows for adjusting the latency of the audio samples
auto capture_result = _fill_buffer();
if(capture_result != capture_e::ok) {
return capture_result;
}
}
switch(channels_out) {
case 2:
pipe->to_stereo(sample_out, sample_buf);
break;
case 6:
pipe->to_51(sample_out, sample_buf);
break;
case 8:
pipe->to_71(sample_out, sample_buf);
break;
default:
BOOST_LOG(error) << "converting to ["sv << channels_out << "] channels is not supported"sv;
return capture_e::error;
}
// The excess samples should be in front of the queue
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) {
BOOST_LOG(debug) << "Trying audio format ["sv << format.name << ']';
audio_client = make_audio_client(device, format, sample_rate);
if(audio_client) {
BOOST_LOG(debug) << "Found audio format ["sv << format.name << ']';
channels_in = format.channels;
this->channels_out = channels_out;
switch(channels_in) {
case 1:
pipe = std::make_unique<mono_t>();
break;
case 2:
pipe = std::make_unique<stereo_t>();
break;
case 6:
pipe = std::make_unique<surr51_t>();
break;
case 8:
pipe = std::make_unique<surr71_t>();
break;
default:
BOOST_LOG(error) << "converting from ["sv << channels_in << "] channels is not supported"sv;
return -1;
}
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::int16_t> { std::max(frames, frame_size) * 2 * channels_in };
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_in);
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;
}
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<std::int16_t> sample_buf;
std::int16_t *sample_buf_pos;
// out --> our audio output
int channels_out;
// in --> our wasapi input
int channels_in;
std::unique_ptr<audio_pipe_t> pipe;
};
class audio_control_t : public ::platf::audio_control_t {
public:
std::optional<sink_t> 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);
auto type = validate_device(device, SAMPLE_RATE);
if(type == format_t::none) {
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;
}
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override {
auto mic = std::make_unique<mic_wasapi_t>();
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.
*
* Any virtual sink detected will be prefixed by:
* virtual-(format name)
* If it doesn't contain that prefix, then the format will not be changed
*/
std::optional<std::wstring> 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());
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;
}
}
}
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;
}
if(init_wave_format(wave_format, SAMPLE_RATE)) {
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;
}
}
return failure;
}
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() << ']';
return -1;
}
return 0;
}
~audio_control_t() override {}
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_t> audio_control() {
auto control = std::make_unique<audio::audio_control_t>();
if(control->init()) {
return nullptr;
}
return control;
}
std::unique_ptr<deinit_t> init() {
if(dxgi::init()) {
return nullptr;
}
return std::make_unique<platf::audio::co_init_t>();
}
} // namespace platf

View File

@@ -0,0 +1,177 @@
//
// Created by loki on 4/23/20.
//
#ifndef SUNSHINE_DISPLAY_H
#define SUNSHINE_DISPLAY_H
#include <d3d11.h>
#include <d3d11_4.h>
#include <d3dcommon.h>
#include <dwmapi.h>
#include <dxgi.h>
#include <dxgi1_2.h>
#include "src/platform/common.h"
#include "src/utility.h"
namespace platf::dxgi {
extern const char *format_str[];
template<class T>
void Release(T *dxgi) {
dxgi->Release();
}
using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>;
using dxgi_t = util::safe_ptr<IDXGIDevice, Release<IDXGIDevice>>;
using dxgi1_t = util::safe_ptr<IDXGIDevice1, Release<IDXGIDevice1>>;
using device_t = util::safe_ptr<ID3D11Device, Release<ID3D11Device>>;
using device_ctx_t = util::safe_ptr<ID3D11DeviceContext, Release<ID3D11DeviceContext>>;
using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;
using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;
using output1_t = util::safe_ptr<IDXGIOutput1, Release<IDXGIOutput1>>;
using dup_t = util::safe_ptr<IDXGIOutputDuplication, Release<IDXGIOutputDuplication>>;
using texture2d_t = util::safe_ptr<ID3D11Texture2D, Release<ID3D11Texture2D>>;
using texture1d_t = util::safe_ptr<ID3D11Texture1D, Release<ID3D11Texture1D>>;
using resource_t = util::safe_ptr<IDXGIResource, Release<IDXGIResource>>;
using multithread_t = util::safe_ptr<ID3D11Multithread, Release<ID3D11Multithread>>;
using vs_t = util::safe_ptr<ID3D11VertexShader, Release<ID3D11VertexShader>>;
using ps_t = util::safe_ptr<ID3D11PixelShader, Release<ID3D11PixelShader>>;
using blend_t = util::safe_ptr<ID3D11BlendState, Release<ID3D11BlendState>>;
using input_layout_t = util::safe_ptr<ID3D11InputLayout, Release<ID3D11InputLayout>>;
using render_target_t = util::safe_ptr<ID3D11RenderTargetView, Release<ID3D11RenderTargetView>>;
using shader_res_t = util::safe_ptr<ID3D11ShaderResourceView, Release<ID3D11ShaderResourceView>>;
using buf_t = util::safe_ptr<ID3D11Buffer, Release<ID3D11Buffer>>;
using raster_state_t = util::safe_ptr<ID3D11RasterizerState, Release<ID3D11RasterizerState>>;
using sampler_state_t = util::safe_ptr<ID3D11SamplerState, Release<ID3D11SamplerState>>;
using blob_t = util::safe_ptr<ID3DBlob, Release<ID3DBlob>>;
using depth_stencil_state_t = util::safe_ptr<ID3D11DepthStencilState, Release<ID3D11DepthStencilState>>;
using depth_stencil_view_t = util::safe_ptr<ID3D11DepthStencilView, Release<ID3D11DepthStencilView>>;
namespace video {
using device_t = util::safe_ptr<ID3D11VideoDevice, Release<ID3D11VideoDevice>>;
using ctx_t = util::safe_ptr<ID3D11VideoContext, Release<ID3D11VideoContext>>;
using processor_t = util::safe_ptr<ID3D11VideoProcessor, Release<ID3D11VideoProcessor>>;
using processor_out_t = util::safe_ptr<ID3D11VideoProcessorOutputView, Release<ID3D11VideoProcessorOutputView>>;
using processor_in_t = util::safe_ptr<ID3D11VideoProcessorInputView, Release<ID3D11VideoProcessorInputView>>;
using processor_enum_t = util::safe_ptr<ID3D11VideoProcessorEnumerator, Release<ID3D11VideoProcessorEnumerator>>;
} // namespace video
class hwdevice_t;
struct cursor_t {
std::vector<std::uint8_t> 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;
}
void set_texture(LONG width, LONG height, texture2d_t &&texture) {
cursor_view.Width = width;
cursor_view.Height = height;
this->texture = std::move(texture);
}
texture2d_t texture;
shader_res_t input_res;
D3D11_VIEWPORT cursor_view;
bool visible;
};
class duplication_t {
public:
dup_t dup;
bool has_frame {};
bool use_dwmflush {};
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();
~duplication_t();
};
class display_base_t : public display_t {
public:
int init(int framerate, const std::string &display_name);
std::chrono::nanoseconds delay;
factory1_t factory;
adapter_t adapter;
output_t output;
device_t device;
device_ctx_t device_ctx;
duplication_t dup;
DXGI_FORMAT format;
D3D_FEATURE_LEVEL feature_level;
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;
typedef NTSTATUS WINAPI (*PD3DKMTSetProcessSchedulingPriorityClass)(HANDLE, D3DKMT_SCHEDULINGPRIORITYCLASS);
};
class display_ram_t : public display_base_t {
public:
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) override;
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible);
std::shared_ptr<img_t> alloc_img() override;
int dummy_img(img_t *img) override;
int init(int framerate, const std::string &display_name);
cursor_t cursor;
D3D11_MAPPED_SUBRESOURCE img_info;
texture2d_t texture;
};
class display_vram_t : public display_base_t, public std::enable_shared_from_this<display_vram_t> {
public:
capture_e capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<img_t> img, bool *cursor) override;
capture_e snapshot(img_t *img, std::chrono::milliseconds timeout, bool cursor_visible);
std::shared_ptr<img_t> alloc_img() override;
int dummy_img(img_t *img_base) override;
int init(int framerate, const std::string &display_name);
std::shared_ptr<platf::hwdevice_t> make_hwdevice(pix_fmt_e pix_fmt) override;
sampler_state_t sampler_linear;
blend_t blend_enable;
blend_t blend_disable;
ps_t scene_ps;
vs_t scene_vs;
texture2d_t src;
gpu_cursor_t cursor;
};
} // namespace platf::dxgi
#endif

View File

@@ -0,0 +1,533 @@
//
// Created by loki on 1/12/20.
//
#include <cmath>
#include <codecvt>
#include "display.h"
#include "misc.h"
#include "src/config.h"
#include "src/main.h"
#include "src/platform/common.h"
namespace platf {
using namespace std::literals;
}
namespace platf::dxgi {
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);
return capture_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;
}
}
duplication_t::~duplication_t() {
release_frame();
}
int display_base_t::init(int framerate, const std::string &display_name) {
/* Uncomment when use of IDXGIOutput5 is implemented
std::call_once(windows_cpp_once_flag, []() {
DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);
const auto DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = ((DPI_AWARENESS_CONTEXT)-4);
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 } / framerate;
// Get rectangle of full desktop for absolute mouse coordinates
env_width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
env_height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
HRESULT status;
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<std::codecvt_utf8_utf16<wchar_t>, 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;
}
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 };
DXGI_OUTPUT_DESC desc;
output_tmp->GetDesc(&desc);
if(!output_name.empty() && desc.DeviceName != output_name) {
continue;
}
if(desc.AttachedToDesktop) {
output = std::move(output_tmp);
offset_x = desc.DesktopCoordinates.left;
offset_y = desc.DesktopCoordinates.top;
width = desc.DesktopCoordinates.right - offset_x;
height = desc.DesktopCoordinates.bottom - offset_y;
// 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;
}
}
if(!output) {
BOOST_LOG(error) << "Failed to locate an output device"sv;
return -1;
}
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_VIDEO_SUPPORT,
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 = 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 && !(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.";
}
}
}
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;
}
dxgi->SetGPUThreadPriority(7);
}
// 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
//TODO: Use IDXGIOutput5 for improved performance
{
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;
}
// We try this twice, in case we still get an error on reinitialization
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);
format = dup_desc.ModeDesc.Format;
BOOST_LOG(debug) << "Source format ["sv << format_str[dup_desc.ModeDesc.Format] << ']';
return 0;
}
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"
};
} // namespace platf::dxgi
namespace platf {
std::shared_ptr<display_t> display(mem_type_e hwdevice_type, const std::string &display_name, int framerate) {
if(hwdevice_type == mem_type_e::dxgi) {
auto disp = std::make_shared<dxgi::display_vram_t>();
if(!disp->init(framerate, display_name)) {
return disp;
}
}
else if(hwdevice_type == mem_type_e::system) {
auto disp = std::make_shared<dxgi::display_ram_t>();
if(!disp->init(framerate, display_name)) {
return disp;
}
}
return nullptr;
}
std::vector<std::string> display_names(mem_type_e) {
std::vector<std::string> display_names;
HRESULT status;
BOOST_LOG(debug) << "Detecting monitors..."sv;
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
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() << ']' << std::endl;
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;
display_names.emplace_back(std::move(device_name));
}
}
return display_names;
}
} // namespace platf

View File

@@ -0,0 +1,327 @@
#include "display.h"
#include "src/main.h"
namespace platf {
using namespace std::literals;
}
namespace platf::dxgi {
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;
// 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);
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 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;
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 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 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;
}
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;
}
++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::capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<::platf::img_t> img, bool *cursor) {
auto next_frame = std::chrono::steady_clock::now();
while(img) {
auto now = std::chrono::steady_clock::now();
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:
std::this_thread::sleep_for(1ms);
continue;
case platf::capture_e::ok:
img = snapshot_cb(img);
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
return status;
}
}
return capture_e::ok;
}
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;
}
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 has been updated
if(frame_info.LastPresentTime.QuadPart != 0) {
{
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;
}
//Copy from GPU to CPU
device_ctx->CopyResource(texture.get(), src.get());
}
if(img_info.pData) {
device_ctx->Unmap(texture.get(), 0);
img_info.pData = nullptr;
}
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;
}
}
const bool mouse_update =
(frame_info.LastMouseUpdateTime.QuadPart || frame_info.PointerShapeBufferSize > 0) &&
(cursor_visible && cursor.visible);
const bool update_flag = frame_info.LastPresentTime.QuadPart != 0 || mouse_update;
if(!update_flag) {
return capture_e::timeout;
}
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);
}
return capture_e::ok;
}
std::shared_ptr<platf::img_t> display_ram_t::alloc_img() {
auto img = std::make_shared<img_t>();
img->pixel_pitch = 4;
img->row_pitch = img_info.RowPitch;
img->width = width;
img->height = height;
img->data = new std::uint8_t[img->row_pitch * height];
return img;
}
int display_ram_t::dummy_img(platf::img_t *img) {
return 0;
}
int display_ram_t::init(int framerate, const std::string &display_name) {
if(display_base_t::init(framerate, display_name)) {
return -1;
}
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 = format;
t.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
auto status = device->CreateTexture2D(&t, nullptr, &texture);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create texture [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
// map the texture simply to get the pitch and stride
status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to map the texture [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
return 0;
}
} // namespace platf::dxgi

View File

@@ -0,0 +1,888 @@
#include <cmath>
#include <codecvt>
#include <d3dcompiler.h>
#include <directxmath.h>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/hwcontext_d3d11va.h>
}
#include "display.h"
#include "src/main.h"
#include "src/video.h"
#define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR "/shaders/directx"
namespace platf {
using namespace std::literals;
}
static void free_frame(AVFrame *frame) {
av_frame_free(&frame);
}
using frame_t = util::safe_ptr<AVFrame, free_frame>;
namespace platf::dxgi {
template<class T>
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_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) {
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;
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 scene_vs_hlsl;
blob_t convert_Y_ps_hlsl;
blob_t scene_ps_hlsl;
struct img_d3d_t : public platf::img_t {
std::shared_ptr<platf::display_t> display;
shader_res_t input_res;
render_target_t scene_rt;
texture2d_t texture;
~img_d3d_t() override = default;
};
util::buffer_t<std::uint8_t> make_cursor_image(util::buffer_t<std::uint8_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;
switch(shape_info.Type) {
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR:
std::for_each((std::uint32_t *)std::begin(img_data), (std::uint32_t *)std::end(img_data), [](auto &pixel) {
if(pixel & 0xFF000000) {
pixel = transparent;
}
});
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR:
return std::move(img_data);
default:
break;
}
shape_info.Height /= 2;
util::buffer_t<std::uint8_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: //black
*pixel_data = black;
break;
case 2: //white
*pixel_data = white;
break;
case 1: //transparent
{
*pixel_data = transparent;
break;
}
case 3: //inverse
{
auto top_p = pixel_data - shape_info.Width;
auto left_p = pixel_data - 1;
auto right_p = pixel_data + 1;
auto bottom_p = pixel_data + shape_info.Width;
// Get the x coordinate of the pixel
auto column = (pixel_data - pixel_begin) % shape_info.Width != 0;
if(top_p >= pixel_begin && *top_p == transparent) {
*top_p = black;
}
if(column != 0 && left_p >= pixel_begin && *left_p == transparent) {
*left_p = black;
}
if(bottom_p < (std::uint32_t *)std::end(cursor_img)) {
*bottom_p = black;
}
if(column != shape_info.Width - 1) {
*right_p = black;
}
*pixel_data = white;
}
}
++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;
#ifndef NDEBUG
flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, 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);
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");
}
int init_rt(device_t::pointer device, shader_res_t &shader_res, render_target_t &render_target, int width, int height, DXGI_FORMAT format, texture2d_t::pointer tex) {
D3D11_SHADER_RESOURCE_VIEW_DESC shader_resource_desc {
format,
D3D11_SRV_DIMENSION_TEXTURE2D
};
shader_resource_desc.Texture2D.MipLevels = 1;
auto status = device->CreateShaderResourceView(tex, &shader_resource_desc, &shader_res);
if(status) {
BOOST_LOG(error) << "Failed to create render target texture for luma [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
D3D11_RENDER_TARGET_VIEW_DESC render_target_desc {
format,
D3D11_RTV_DIMENSION_TEXTURE2D
};
status = device->CreateRenderTargetView(tex, &render_target_desc, &render_target);
if(status) {
BOOST_LOG(error) << "Failed to create render target view [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
return 0;
}
int init_rt(device_t::pointer device, shader_res_t &shader_res, render_target_t &render_target, int width, int height, DXGI_FORMAT format) {
D3D11_TEXTURE2D_DESC desc {};
desc.Width = width;
desc.Height = height;
desc.Format = format;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.SampleDesc.Count = 1;
texture2d_t tex;
auto status = device->CreateTexture2D(&desc, nullptr, &tex);
if(status) {
BOOST_LOG(error) << "Failed to create render target texture for luma [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
return init_rt(device, shader_res, render_target, width, height, format, tex.get());
}
class hwdevice_t : public platf::hwdevice_t {
public:
int convert(platf::img_t &img_base) override {
auto &img = (img_d3d_t &)img_base;
device_ctx_p->IASetInputLayout(input_layout.get());
_init_view_port(this->img.width, this->img.height);
device_ctx_p->OMSetRenderTargets(1, &nv12_Y_rt, nullptr);
device_ctx_p->VSSetShader(scene_vs.get(), nullptr, 0);
device_ctx_p->PSSetShader(convert_Y_ps.get(), nullptr, 0);
device_ctx_p->PSSetShaderResources(0, 1, &back_img.input_res);
device_ctx_p->Draw(3, 0);
device_ctx_p->RSSetViewports(1, &outY_view);
device_ctx_p->PSSetShaderResources(0, 1, &img.input_res);
device_ctx_p->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_p->Flush();
_init_view_port(this->img.width / 2, this->img.height / 2);
device_ctx_p->OMSetRenderTargets(1, &nv12_UV_rt, nullptr);
device_ctx_p->VSSetShader(convert_UV_vs.get(), nullptr, 0);
device_ctx_p->PSSetShader(convert_UV_ps.get(), nullptr, 0);
device_ctx_p->PSSetShaderResources(0, 1, &back_img.input_res);
device_ctx_p->Draw(3, 0);
device_ctx_p->RSSetViewports(1, &outUV_view);
device_ctx_p->PSSetShaderResources(0, 1, &img.input_res);
device_ctx_p->Draw(3, 0);
device_ctx_p->Flush();
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
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_p->PSSetConstantBuffers(0, 1, &color_matrix);
this->color_matrix = std::move(color_matrix);
}
int set_frame(AVFrame *frame) {
this->hwframe.reset(frame);
this->frame = frame;
auto device_p = (device_t::pointer)data;
auto out_width = frame->width;
auto out_height = frame->height;
float in_width = img.display->width;
float in_height = img.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 };
D3D11_TEXTURE2D_DESC t {};
t.Width = out_width;
t.Height = out_height;
t.MipLevels = 1;
t.ArraySize = 1;
t.SampleDesc.Count = 1;
t.Usage = D3D11_USAGE_DEFAULT;
t.Format = format;
t.BindFlags = D3D11_BIND_RENDER_TARGET;
auto status = device_p->CreateTexture2D(&t, nullptr, &img.texture);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create render target texture [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
img.width = out_width;
img.height = out_height;
img.data = (std::uint8_t *)img.texture.get();
img.row_pitch = out_width * 4;
img.pixel_pitch = 4;
float info_in[16 / sizeof(float)] { 1.0f / (float)out_width }; //aligned to 16-byte
info_scene = make_buffer(device_p, info_in);
if(!info_in) {
BOOST_LOG(error) << "Failed to create info scene buffer"sv;
return -1;
}
D3D11_RENDER_TARGET_VIEW_DESC nv12_rt_desc {
DXGI_FORMAT_R8_UNORM,
D3D11_RTV_DIMENSION_TEXTURE2D
};
status = device_p->CreateRenderTargetView(img.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 = DXGI_FORMAT_R8G8_UNORM;
status = device_p->CreateRenderTargetView(img.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;
}
// Need to have something refcounted
if(!frame->buf[0]) {
frame->buf[0] = av_buffer_allocz(sizeof(AVD3D11FrameDescriptor));
}
auto desc = (AVD3D11FrameDescriptor *)frame->buf[0]->data;
desc->texture = (ID3D11Texture2D *)img.data;
desc->index = 0;
frame->data[0] = img.data;
frame->data[1] = 0;
frame->linesize[0] = img.row_pitch;
frame->height = img.height;
frame->width = img.width;
return 0;
}
int init(
std::shared_ptr<platf::display_t> display, device_t::pointer device_p, device_ctx_t::pointer device_ctx_p,
pix_fmt_e pix_fmt) {
HRESULT status;
device_p->AddRef();
data = device_p;
this->device_ctx_p = device_ctx_p;
format = (pix_fmt == pix_fmt_e::nv12 ? DXGI_FORMAT_NV12 : DXGI_FORMAT_P010);
status = device_p->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_p->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_p->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;
}
status = device_p->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;
}
status = device_p->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;
}
color_matrix = make_buffer(device_p, ::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_p->CreateInputLayout(
&layout_desc, 1,
convert_UV_vs_hlsl->GetBufferPointer(), convert_UV_vs_hlsl->GetBufferSize(),
&input_layout);
img.display = std::move(display);
// Color the background black, so that the padding for keeping the aspect ratio
// is black
if(img.display->dummy_img(&back_img)) {
BOOST_LOG(warning) << "Couldn't create an image to set background color to black"sv;
return -1;
}
D3D11_SHADER_RESOURCE_VIEW_DESC desc {
DXGI_FORMAT_B8G8R8A8_UNORM,
D3D11_SRV_DIMENSION_TEXTURE2D
};
desc.Texture2D.MipLevels = 1;
status = device_p->CreateShaderResourceView(back_img.texture.get(), &desc, &back_img.input_res);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create input shader resource view [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
device_ctx_p->IASetInputLayout(input_layout.get());
device_ctx_p->PSSetConstantBuffers(0, 1, &color_matrix);
device_ctx_p->VSSetConstantBuffers(0, 1, &info_scene);
return 0;
}
~hwdevice_t() override {
if(data) {
((ID3D11Device *)data)->Release();
}
}
private:
void _init_view_port(float x, float y, float width, float height) {
D3D11_VIEWPORT view {
x, y,
width, height,
0.0f, 1.0f
};
device_ctx_p->RSSetViewports(1, &view);
}
void _init_view_port(float width, float height) {
_init_view_port(0.0f, 0.0f, width, height);
}
public:
frame_t hwframe;
::video::color_t *color_p;
buf_t info_scene;
buf_t color_matrix;
input_layout_t input_layout;
render_target_t nv12_Y_rt;
render_target_t nv12_UV_rt;
// The image referenced by hwframe
// The resulting image is stored here.
img_d3d_t img;
// Clear nv12 render target to black
img_d3d_t back_img;
vs_t convert_UV_vs;
ps_t convert_UV_ps;
ps_t convert_Y_ps;
ps_t scene_ps;
vs_t scene_vs;
D3D11_VIEWPORT outY_view;
D3D11_VIEWPORT outUV_view;
DXGI_FORMAT format;
device_ctx_t::pointer device_ctx_p;
};
capture_e display_vram_t::capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<::platf::img_t> img, bool *cursor) {
auto next_frame = std::chrono::steady_clock::now();
while(img) {
auto now = std::chrono::steady_clock::now();
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:
std::this_thread::sleep_for(1ms);
continue;
case platf::capture_e::ok:
img = snapshot_cb(img);
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
return status;
}
}
return capture_e::ok;
}
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;
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<std::uint8_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 cursor_img = make_cursor_image(std::move(img_data), shape_info);
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_DEFAULT;
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 capture_e::error;
}
D3D11_SHADER_RESOURCE_VIEW_DESC desc {
DXGI_FORMAT_B8G8R8A8_UNORM,
D3D11_SRV_DIMENSION_TEXTURE2D
};
desc.Texture2D.MipLevels = 1;
// Free resources before allocating on the next line.
cursor.input_res.reset();
status = device->CreateShaderResourceView(texture.get(), &desc, &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 capture_e::error;
}
cursor.set_texture(t.Width, t.Height, std::move(texture));
}
if(frame_info.LastMouseUpdateTime.QuadPart) {
cursor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y, frame_info.PointerPosition.Visible && cursor_visible);
}
if(frame_update_flag) {
src.reset();
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;
}
}
device_ctx->CopyResource(img->texture.get(), src.get());
if(cursor.visible) {
D3D11_VIEWPORT view {
0.0f, 0.0f,
(float)width, (float)height,
0.0f, 1.0f
};
device_ctx->VSSetShader(scene_vs.get(), nullptr, 0);
device_ctx->PSSetShader(scene_ps.get(), nullptr, 0);
device_ctx->RSSetViewports(1, &view);
device_ctx->OMSetRenderTargets(1, &img->scene_rt, nullptr);
device_ctx->PSSetShaderResources(0, 1, &cursor.input_res);
device_ctx->OMSetBlendState(blend_enable.get(), nullptr, 0xFFFFFFFFu);
device_ctx->RSSetViewports(1, &cursor.cursor_view);
device_ctx->Draw(3, 0);
device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);
}
return capture_e::ok;
}
int display_vram_t::init(int framerate, const std::string &display_name) {
if(display_base_t::init(framerate, 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;
}
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_enable = make_blend(device.get(), true);
blend_disable = make_blend(device.get(), false);
if(!blend_disable || !blend_enable) {
return -1;
}
device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);
device_ctx->PSSetSamplers(0, 1, &sampler_linear);
device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
return 0;
}
std::shared_ptr<platf::img_t> display_vram_t::alloc_img() {
auto img = std::make_shared<img_d3d_t>();
img->pixel_pitch = 4;
img->row_pitch = img->pixel_pitch * width;
img->width = width;
img->height = height;
img->display = shared_from_this();
auto dummy_data = std::make_unique<std::uint8_t[]>(img->row_pitch * height);
D3D11_SUBRESOURCE_DATA data {
dummy_data.get(),
(UINT)img->row_pitch
};
std::fill_n(dummy_data.get(), img->row_pitch * height, 0);
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 = format;
t.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
auto status = device->CreateTexture2D(&t, &data, &img->texture);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create img buf texture [0x"sv << util::hex(status).to_string_view() << ']';
return nullptr;
}
if(init_rt(device.get(), img->input_res, img->scene_rt, width, height, format, img->texture.get())) {
return nullptr;
}
img->data = (std::uint8_t *)img->texture.get();
return img;
}
int display_vram_t::dummy_img(platf::img_t *img_base) {
auto img = (img_d3d_t *)img_base;
if(img->texture) {
return 0;
}
img->row_pitch = width * 4;
auto dummy_data = std::make_unique<int[]>(width * height);
D3D11_SUBRESOURCE_DATA data {
dummy_data.get(),
(UINT)img->row_pitch
};
std::fill_n(dummy_data.get(), width * height, 0);
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 = format;
t.BindFlags = D3D11_BIND_SHADER_RESOURCE;
dxgi::texture2d_t tex;
auto status = device->CreateTexture2D(&t, &data, &tex);
if(FAILED(status)) {
BOOST_LOG(error) << "Failed to create dummy texture [0x"sv << util::hex(status).to_string_view() << ']';
return -1;
}
img->texture = std::move(tex);
img->data = (std::uint8_t *)img->texture.get();
return 0;
}
std::shared_ptr<platf::hwdevice_t> display_vram_t::make_hwdevice(pix_fmt_e pix_fmt) {
if(pix_fmt != platf::pix_fmt_e::nv12) {
BOOST_LOG(error) << "display_vram_t doesn't support pixel format ["sv << from_pix_fmt(pix_fmt) << ']';
return nullptr;
}
auto hwdevice = std::make_shared<hwdevice_t>();
auto ret = hwdevice->init(
shared_from_this(),
device.get(),
device_ctx.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_UV_ps_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR "/ConvertUVPS.hlsl");
if(!convert_UV_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;
}
BOOST_LOG(info) << "Compiled shaders"sv;
return 0;
}
} // namespace platf::dxgi

View File

@@ -0,0 +1,491 @@
#include <windows.h>
#include <cmath>
#include <ViGEm/Client.h>
#include "misc.h"
#include "src/config.h"
#include "src/main.h"
#include "src/platform/common.h"
namespace platf {
using namespace std::literals;
thread_local HDESK _lastKnownInputDesktop = nullptr;
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>;
static VIGEM_TARGET_TYPE map(const std::string_view &gp) {
if(gp == "x360"sv) {
return Xbox360Wired;
}
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 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;
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() << ']';
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() << ']';
}
}
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<std::pair<rumble_queue_t, target_t>> 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);
}
input_t input() {
input_t result { new vigem_t {} };
auto vigem = (vigem_t *)result.get();
if(vigem->init()) {
return nullptr;
}
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;
}
}
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 keyboard(input_t &input, uint16_t modcode, bool release) {
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) {
ki.wScan = MapVirtualKey(modcode, MAPVK_VK_TO_VSC);
ki.dwFlags = KEYEVENTF_SCANCODE;
}
else {
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);
}
int alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) {
if(!input) {
return 0;
}
return ((vigem_t *)input.get())->alloc_gamepad_interal(nr, rumble_queue, map(config::input.gamepad));
}
void free_gamepad(input_t &input, int nr) {
if(!input) {
return;
}
((vigem_t *)input.get())->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(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;
if(flags & RIGHT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_RIGHT;
if(flags & START) buttons |= DS4_BUTTON_OPTIONS;
if(flags & A) buttons |= DS4_BUTTON_CROSS;
if(flags & B) buttons |= DS4_BUTTON_CIRCLE;
if(flags & X) buttons |= DS4_BUTTON_SQUARE;
if(flags & Y) buttons |= DS4_BUTTON_TRIANGLE;
if(gamepad_state.lt > 0) buttons |= DS4_BUTTON_TRIGGER_LEFT;
if(gamepad_state.rt > 0) buttons |= DS4_BUTTON_TRIGGER_RIGHT;
// 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<std::uint16_t>::max() / 2 + 1) / 257;
}
static std::uint8_t to_ds4_triggerY(std::int16_t v) {
auto new_v = -((std::numeric_limits<std::uint16_t>::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) {
// If there is no gamepad support
if(!input) {
return;
}
auto vigem = (vigem_t *)input.get();
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(fatal) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']';
log_flush();
std::abort();
}
}
void freeInput(void *p) {
auto vigem = (vigem_t *)p;
delete vigem;
}
std::vector<std::string_view> &supported_gamepads() {
// ds4 == ps4
static std::vector<std::string_view> gps {
"x360"sv, "ds4"sv, "ps4"sv
};
return gps;
}
} // namespace platf

View File

@@ -0,0 +1,123 @@
#include <filesystem>
#include <iomanip>
#include <sstream>
// prevent clang format from "optimizing" the header include order
// clang-format off
#include <winsock2.h>
#include <iphlpapi.h>
#include <windows.h>
#include <winuser.h>
#include <ws2tcpip.h>
// clang-format on
#include "src/main.h"
#include "src/utility.h"
using namespace std::literals;
namespace platf {
using adapteraddrs_t = util::c_ptr<IP_ADAPTER_ADDRESSES>;
std::filesystem::path appdata() {
return L"."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);
}
if(family == AF_INET) {
inet_ntop(AF_INET, &((sockaddr_in *)socket_address)->sin_addr, data, INET_ADDRSTRLEN);
}
return std::string { data };
}
std::pair<std::uint16_t, std::string> 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 } };
}
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 << ':';
}
mac_addr << std::setw(2) << std::setfill('0') << (int)adapter_pos->PhysicalAddress[i];
}
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;
}
if(!SetThreadDesktop(hDesk)) {
auto err = GetLastError();
BOOST_LOG(error) << "Failed to sync desktop to thread [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 };
}
} // namespace platf

View File

@@ -0,0 +1,13 @@
#ifndef SUNSHINE_WINDOWS_MISC_H
#define SUNSHINE_WINDOWS_MISC_H
#include <string_view>
#include <windows.h>
#include <winnt.h>
namespace platf {
void print_status(const std::string_view &prefix, HRESULT status);
HDESK syncThreadDesktop();
} // namespace platf
#endif

View File

@@ -0,0 +1,195 @@
#include <winsock2.h>
#include <windows.h>
#include <windns.h>
#include <winerror.h>
#include <boost/asio/ip/host_name.hpp>
#include "misc.h"
#include "src/config.h"
#include "src/main.h"
#include "src/network.h"
#include "src/nvhttp.h"
#include "src/platform/common.h"
#include "src/thread_safe.h"
#define _FN(x, ret, args) \
typedef ret(*x##_fn) args; \
static x##_fn x
using namespace std::literals;
#define __SV(quote) L##quote##sv
#define SV(quote) __SV(quote)
extern "C" {
#ifndef __MINGW32__
constexpr auto DNS_REQUEST_PENDING = 9506L;
constexpr auto DNS_QUERY_REQUEST_VERSION1 = 0x1;
constexpr auto DNS_QUERY_RESULTS_VERSION1 = 0x1;
#endif
#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);
#ifndef __MINGW32__
typedef struct _DNS_SERVICE_INSTANCE {
LPWSTR pszInstanceName;
LPWSTR pszHostName;
IP4_ADDRESS *ip4Address;
IP6_ADDRESS *ip6Address;
WORD wPort;
WORD wPriority;
WORD wWeight;
// Property list
DWORD dwPropertyCount;
PWSTR *keys;
PWSTR *values;
DWORD dwInterfaceIndex;
} DNS_SERVICE_INSTANCE, *PDNS_SERVICE_INSTANCE;
#endif
typedef VOID WINAPI DNS_SERVICE_REGISTER_COMPLETE(
_In_ DWORD Status,
_In_ PVOID pQueryContext,
_In_ PDNS_SERVICE_INSTANCE pInstance);
typedef DNS_SERVICE_REGISTER_COMPLETE *PDNS_SERVICE_REGISTER_COMPLETE;
#ifndef __MINGW32__
typedef struct _DNS_SERVICE_CANCEL {
PVOID reserved;
} DNS_SERVICE_CANCEL, *PDNS_SERVICE_CANCEL;
typedef struct _DNS_SERVICE_REGISTER_REQUEST {
ULONG Version;
ULONG InterfaceIndex;
PDNS_SERVICE_INSTANCE pServiceInstance;
PDNS_SERVICE_REGISTER_COMPLETE pRegisterCompletionCallback;
PVOID pQueryContext;
HANDLE hCredentials;
BOOL unicastEnabled;
} DNS_SERVICE_REGISTER_REQUEST, *PDNS_SERVICE_REGISTER_REQUEST;
#endif
_FN(_DnsServiceFreeInstance, VOID, (_In_ PDNS_SERVICE_INSTANCE pInstance));
_FN(_DnsServiceDeRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel));
_FN(_DnsServiceRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel));
} /* extern "C" */
namespace platf::publish {
VOID WINAPI register_cb(DWORD status, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) {
auto alarm = (safe::alarm_t<DNS_STATUS>::element_type *)pQueryContext;
auto fg = util::fail_guard([&]() {
if(pInstance) {
_DnsServiceFreeInstance(pInstance);
}
});
if(status) {
print_status("register_cb()"sv, status);
alarm->ring(-1);
return;
}
alarm->ring(0);
}
static int service(bool enable) {
auto alarm = safe::make_alarm<DNS_STATUS>();
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, 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 = &instance;
req.pRegisterCompletionCallback = register_cb;
DNS_STATUS status {};
if(enable) {
status = _DnsServiceRegister(&req, nullptr);
}
else {
status = _DnsServiceDeRegister(&req, nullptr);
}
alarm->wait();
status = *alarm->status();
if(status) {
BOOST_LOG(error) << "No mDNS service"sv;
return -1;
}
return 0;
}
class deinit_t : public ::platf::deinit_t {
public:
~deinit_t() override {
if(service(false)) {
std::abort();
}
BOOST_LOG(info) << "Unregistered Sunshine Gamestream service"sv;
}
};
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;
}
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;
}
if(service(true)) {
return nullptr;
}
BOOST_LOG(info) << "Registered Sunshine Gamestream service"sv;
return std::make_unique<deinit_t>();
}
} // namespace platf::publish

View File

@@ -0,0 +1 @@
SuperDuperAmazing ICON DISCARDABLE "@SUNSHINE_ICON_PATH@"

390
src/process.cpp Normal file
View File

@@ -0,0 +1,390 @@
//
// Created by loki on 12/14/19.
//
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
#include "process.h"
#include <filesystem>
#include <string>
#include <vector>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include "main.h"
#include "utility.h"
namespace proc {
using namespace std::literals;
namespace bp = boost::process;
namespace pt = boost::property_tree;
proc_t proc;
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();
}
int exe(const std::string &cmd, bp::environment &env, file_t &file, std::error_code &ec) {
if(!file) {
return bp::system(cmd, env, bp::std_out > bp::null, bp::std_err > bp::null, ec);
}
return bp::system(cmd, env, bp::std_out > file.get(), bp::std_err > file.get(), ec);
}
int proc_t::execute(int app_id) {
if(!running() && _app_id != -1) {
// previous process exited on it's own, reset _process_handle
_process_handle = bp::group();
_app_id = -1;
}
if(app_id < 0 || app_id >= _apps.size()) {
BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']';
return 404;
}
// Ensure starting from a clean slate
terminate();
_app_id = app_id;
auto &proc = _apps[app_id];
_undo_begin = std::begin(proc.prep_cmds);
_undo_it = _undo_begin;
if(!proc.output.empty() && proc.output != "null"sv) {
_pipe.reset(fopen(proc.output.c_str(), "a"));
}
std::error_code ec;
//Executed when returning from function
auto fg = util::fail_guard([&]() {
terminate();
});
for(; _undo_it != std::end(proc.prep_cmds); ++_undo_it) {
auto &cmd = _undo_it->do_cmd;
BOOST_LOG(info) << "Executing: ["sv << cmd << ']';
auto ret = exe(cmd, _env, _pipe, ec);
if(ec) {
BOOST_LOG(error) << "Couldn't run ["sv << cmd << "]: System: "sv << ec.message();
return -1;
}
if(ret != 0) {
BOOST_LOG(error) << '[' << cmd << "] failed with code ["sv << ret << ']';
return -1;
}
}
for(auto &cmd : proc.detached) {
BOOST_LOG(info) << "Spawning ["sv << cmd << ']';
if(proc.output.empty() || proc.output == "null"sv) {
bp::spawn(cmd, _env, bp::std_out > bp::null, bp::std_err > bp::null, ec);
}
else {
bp::spawn(cmd, _env, bp::std_out > _pipe.get(), bp::std_err > _pipe.get(), ec);
}
if(ec) {
BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message();
}
}
if(proc.cmd.empty()) {
BOOST_LOG(debug) << "Executing [Desktop]"sv;
placebo = true;
}
else {
boost::filesystem::path working_dir = proc.working_dir.empty() ?
boost::filesystem::path(proc.cmd).parent_path() :
boost::filesystem::path(proc.working_dir);
if(proc.output.empty() || proc.output == "null"sv) {
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']';
_process = bp::child(_process_handle, proc.cmd, _env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec);
}
else {
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']';
_process = bp::child(_process_handle, proc.cmd, _env, bp::start_dir(working_dir), bp::std_out > _pipe.get(), bp::std_err > _pipe.get(), ec);
}
}
if(ec) {
BOOST_LOG(warning) << "Couldn't run ["sv << proc.cmd << "]: System: "sv << ec.message();
return -1;
}
fg.disable();
return 0;
}
int proc_t::running() {
if(placebo || _process.running()) {
return _app_id;
}
return -1;
}
void proc_t::terminate() {
std::error_code ec;
// Ensure child process is terminated
placebo = false;
process_end(_process, _process_handle);
_app_id = -1;
if(ec) {
BOOST_LOG(fatal) << "System: "sv << ec.message();
log_flush();
std::abort();
}
for(; _undo_it != _undo_begin; --_undo_it) {
auto &cmd = (_undo_it - 1)->undo_cmd;
if(cmd.empty()) {
continue;
}
BOOST_LOG(debug) << "Executing: ["sv << cmd << ']';
auto ret = exe(cmd, _env, _pipe, ec);
if(ec) {
BOOST_LOG(fatal) << "System: "sv << ec.message();
log_flush();
std::abort();
}
if(ret != 0) {
BOOST_LOG(fatal) << "Return code ["sv << ret << ']';
log_flush();
std::abort();
}
}
_pipe.reset();
}
const std::vector<ctx_t> &proc_t::get_apps() const {
return _apps;
}
std::vector<ctx_t> &proc_t::get_apps() {
return _apps;
}
/// Gets application image from application list.
/// 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 app_index = app_id - 1;
if(app_index < 0 || app_index >= _apps.size()) {
BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']';
return SUNSHINE_ASSETS_DIR "/box.png";
}
auto app_image_path = _apps[app_index].image_path;
if(app_image_path.empty()) {
return SUNSHINE_ASSETS_DIR "/box.png";
}
auto image_extension = std::filesystem::path(app_image_path).extension().string();
boost::to_lower(image_extension);
std::error_code code;
if(!std::filesystem::exists(app_image_path, code) || image_extension != ".png") {
return SUNSHINE_ASSETS_DIR "/box.png";
}
// return only "content-type" http header compatible image type.
return 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));
ss << env[std::string { var_begin, var_end }].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::optional<proc::proc_t> 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::string>());
}
std::vector<proc::ctx_t> apps;
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 output = app_node.get_optional<std::string>("output"s);
auto name = parse_env_val(this_env, app_node.get<std::string>("name"s));
auto cmd = app_node.get_optional<std::string>("cmd"s);
auto image_path = app_node.get_optional<std::string>("image-path"s);
auto working_dir = app_node.get_optional<std::string>("working-dir"s);
std::vector<proc::cmd_t> prep_cmds;
if(prep_nodes_opt) {
auto &prep_nodes = *prep_nodes_opt;
prep_cmds.reserve(prep_nodes.size());
for(auto &[_, prep_node] : prep_nodes) {
auto do_cmd = parse_env_val(this_env, prep_node.get<std::string>("do"s));
auto undo_cmd = prep_node.get_optional<std::string>("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<std::string> 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<std::string>()));
}
}
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);
}
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::ctx_t ctx;
ctx.name = "Desktop"s;
proc_opt->get_apps().emplace(std::begin(proc_opt->get_apps()), std::move(ctx));
}
proc = std::move(*proc_opt);
}
}
} // namespace proc

108
src/process.h Normal file
View File

@@ -0,0 +1,108 @@
//
// Created by loki on 12/14/19.
//
#ifndef SUNSHINE_PROCESS_H
#define SUNSHINE_PROCESS_H
#ifndef __kernel_entry
#define __kernel_entry
#endif
#include <optional>
#include <unordered_map>
#include <boost/process.hpp>
#include "utility.h"
namespace proc {
using file_t = util::safe_ptr_v2<FILE, int, fclose>;
struct cmd_t {
cmd_t(std::string &&do_cmd, std::string &&undo_cmd) : do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)) {}
explicit cmd_t(std::string &&do_cmd) : do_cmd(std::move(do_cmd)) {}
std::string do_cmd;
// Executed when proc_t has finished running, meant to reverse 'do_cmd' if applicable
std::string undo_cmd;
};
/*
* pre_cmds -- guaranteed to be executed unless any of the commands fail.
* detached -- commands detached from Sunshine
* cmd -- Runs indefinitely until:
* No session is running and a different set of commands it to be executed
* Command exits
* working_dir -- the process working directory. This is required for some games to run properly.
* cmd_output --
* empty -- The output of the commands are appended to the output of sunshine
* "null" -- The output of the commands are discarded
* filename -- The output of the commands are appended to filename
*/
struct ctx_t {
std::vector<cmd_t> 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<std::string> detached;
std::string name;
std::string cmd;
std::string working_dir;
std::string output;
std::string image_path;
};
class proc_t {
public:
KITTY_DEFAULT_CONSTR_MOVE_THROW(proc_t)
proc_t(
boost::process::environment &&env,
std::vector<ctx_t> &&apps) : _app_id(-1),
_env(std::move(env)),
_apps(std::move(apps)) {}
int execute(int app_id);
/**
* @return _app_id if a process is running, otherwise returns -1
*/
int running();
~proc_t();
const std::vector<ctx_t> &get_apps() const;
std::vector<ctx_t> &get_apps();
std::string get_app_image(int app_id);
void terminate();
private:
int _app_id;
boost::process::environment _env;
std::vector<ctx_t> _apps;
// If no command associated with _app_id, yet it's still running
bool placebo {};
boost::process::child _process;
boost::process::group _process_handle;
file_t _pipe;
std::vector<cmd_t>::const_iterator _undo_it;
std::vector<cmd_t>::const_iterator _undo_begin;
};
void refresh(const std::string &file_name);
std::optional<proc::proc_t> parse(const std::string &file_name);
extern proc_t proc;
} // namespace proc
#endif //SUNSHINE_PROCESS_H

156
src/round_robin.h Normal file
View File

@@ -0,0 +1,156 @@
#ifndef KITTY_UTIL_ITERATOR_H
#define KITTY_UTIL_ITERATOR_H
#include <iterator>
namespace util {
template<class V, class T>
class it_wrap_t : public std::iterator<std::random_access_iterator_tag, V> {
public:
typedef T iterator;
typedef typename std::iterator<std::random_access_iterator_tag, V>::value_type class_t;
typedef class_t &reference;
typedef class_t *pointer;
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();
++_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<iterator *>(this); }
const iterator &_this() const { return *static_cast<const iterator *>(this); }
};
template<class V, class It>
class round_robin_t : public it_wrap_t<V, round_robin_t<V, It>> {
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<class V, class It>
round_robin_t<V, It> make_round_robin(It begin, It end) {
return round_robin_t<V, It>(begin, end);
}
} // namespace util
#endif

768
src/rtsp.cpp Normal file
View File

@@ -0,0 +1,768 @@
//
// Created by loki on 2/2/20.
//
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
extern "C" {
#include <moonlight-common-c/src/Rtsp.h>
}
#include <array>
#include <cctype>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include "config.h"
#include "input.h"
#include "main.h"
#include "network.h"
#include "rtsp.h"
#include "stream.h"
#include "sync.h"
#include <unordered_map>
namespace asio = boost::asio;
using asio::ip::tcp;
using asio::ip::udp;
using namespace std::literals;
namespace stream {
void free_msg(PRTSP_MESSAGE msg) {
freeMessage(msg);
delete msg;
}
class rtsp_server_t;
using msg_t = util::safe_ptr<RTSP_MESSAGE, free_msg>;
using cmd_func_t = std::function<void(rtsp_server_t *server, tcp::socket &, msg_t &&)>;
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<socket_t> {
public:
socket_t(boost::asio::io_service &ios, std::function<void(tcp::socket &sock, msg_t &&)> &&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));
}
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(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_payload, shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
}
static void handle_payload(std::shared_ptr<socket_t> &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_lenght = 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_lenght = util::from_chars(begin, std::end(content));
break;
}
}
if(end - socket->crlf >= content_lenght) {
if(end - socket->crlf > content_lenght) {
BOOST_LOG(warning) << "(end - socket->crlf) > content_lenght -- "sv << (std::size_t)(end - socket->crlf) << " > "sv << content_lenght;
}
fg.disable();
print_msg(req.get());
socket->handle_data(std::move(req));
}
socket->begin = end;
}
static void handle_read(std::shared_ptr<socket_t> &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();
handle_payload(socket, ec, buf_size);
}
void handle_data(msg_t &&req) {
handle_data_fn(sock, std::move(req));
}
std::function<void(tcp::socket &sock, msg_t &&)> handle_data_fn;
tcp::socket sock;
std::array<char, 2048> msg_buf;
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;
}
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<socket_t>(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<class T, class X>
void iterate(std::chrono::duration<T, X> 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();
next_socket = std::make_shared<socket_t>(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);
});
}
void map(const std::string_view &type, cmd_func_t cb) {
_map_cmd_cb.emplace(type, std::move(cb));
}
void session_raise(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);
}
int session_count() const {
return config::stream.channels - _slot_count;
}
safe::event_t<launch_session_t> launch_event;
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;
}
}
auto lg = _session_slots.lock();
for(auto &slot : *_session_slots) {
if(slot && (all || session::state(*slot) == session::state_e::STOPPING)) {
session::stop(*slot);
session::join(*slot);
slot.reset();
++_slot_count;
}
}
if(all && !ios.stopped()) {
ios.stop();
}
}
void clear(std::shared_ptr<session_t> *session_p) {
auto lg = _session_slots.lock();
session_p->reset();
++_slot_count;
}
std::shared_ptr<session_t> *accept(std::shared_ptr<session_t> &session) {
auto lg = _session_slots.lock();
for(auto &slot : *_session_slots) {
if(!slot) {
slot = session;
return &slot;
}
}
return nullptr;
}
private:
std::unordered_map<std::string_view, cmd_func_t> _map_cmd_cb;
util::sync_t<std::vector<std::shared_ptr<session_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<socket_t> next_socket;
};
rtsp_server_t server {};
void launch_session_raise(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<char> 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<char *>("RTSP/1.0"), statuscode, const_cast<char *>(status_msg), seqn, options, const_cast<char *>(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<char *>("CSeq");
auto seqn_str = std::to_string(req->sequenceNumber);
option.content = const_cast<char *>(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<char *>("CSeq");
auto seqn_str = std::to_string(req->sequenceNumber);
option.content = const_cast<char *>(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);
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<char *>("CSeq");
auto seqn_str = std::to_string(req->sequenceNumber);
seqn.content = const_cast<char *>(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<char *>("Session");
session_option.content = const_cast<char *>("DEADBEEFCAFE;timeout = 90");
session_option.next = &port_option;
// Moonlight merely requires 'server_port=<port>'
auto port_value = "server_port=" + std::to_string(port);
port_option.option = const_cast<char *>("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<char *>("CSeq");
auto seqn_str = std::to_string(req->sequenceNumber);
option.content = const_cast<char *>(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<std::string_view> 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;
}
}
}
std::string_view client;
std::unordered_map<std::string_view, std::string_view> 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);
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.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;
}
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 = 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(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<char *>("CSeq");
auto seqn_str = std::to_string(req->sequenceNumber);
option.content = const_cast<char *>(seqn_str.c_str());
respond(sock, &option, 200, "OK", req->sequenceNumber, {});
}
void rtpThread() {
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
auto broadcast_shutdown_event = mail::man->event<bool>(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_SETUP_PORT), ec)) {
BOOST_LOG(fatal) << "Couldn't bind RTSP server to port ["sv << map_port(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 stream

30
src/rtsp.h Normal file
View File

@@ -0,0 +1,30 @@
//
// Created by loki on 2/2/20.
//
#ifndef SUNSHINE_RTSP_H
#define SUNSHINE_RTSP_H
#include <atomic>
#include "crypto.h"
#include "thread_safe.h"
namespace stream {
constexpr auto RTSP_SETUP_PORT = 21;
struct launch_session_t {
crypto::aes_t gcm_key;
crypto::aes_t iv;
bool host_audio;
};
void launch_session_raise(launch_session_t launch_session);
int session_count();
void rtpThread();
} // namespace stream
#endif //SUNSHINE_RTSP_H

1472
src/stream.cpp Normal file

File diff suppressed because it is too large Load Diff

48
src/stream.h Normal file
View File

@@ -0,0 +1,48 @@
//
// Created by loki on 6/5/19.
//
#ifndef SUNSHINE_STREAM_H
#define SUNSHINE_STREAM_H
#include <boost/asio.hpp>
#include "audio.h"
#include "crypto.h"
#include "video.h"
namespace stream {
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;
int packetsize;
int minRequiredFecPackets;
int featureFlags;
int controlProtocolType;
std::optional<int> gcmap;
};
namespace session {
enum class state_e : int {
STOPPED,
STOPPING,
STARTING,
RUNNING,
};
std::shared_ptr<session_t> 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

95
src/sync.h Normal file
View File

@@ -0,0 +1,95 @@
//
// Created by loki on 16-4-19.
//
#ifndef SUNSHINE_SYNC_H
#define SUNSHINE_SYNC_H
#include <array>
#include <mutex>
#include <utility>
namespace util {
template<class T, class M = std::mutex>
class sync_t {
public:
using value_t = T;
using mutex_t = M;
std::lock_guard<mutex_t> lock() {
return std::lock_guard { _lock };
}
template<class... Args>
sync_t(Args &&...args) : raw { std::forward<Args>(args)... } {}
sync_t &operator=(sync_t &&other) noexcept {
std::lock(_lock, other._lock);
raw = std::move(other.raw);
_lock.unlock();
other._lock.unlock();
return *this;
}
sync_t &operator=(sync_t &other) noexcept {
std::lock(_lock, other._lock);
raw = other.raw;
_lock.unlock();
other._lock.unlock();
return *this;
}
template<class V>
sync_t &operator=(V &&val) {
auto lg = lock();
raw = val;
return *this;
}
sync_t &operator=(const value_t &val) noexcept {
auto lg = lock();
raw = val;
return *this;
}
sync_t &operator=(value_t &&val) noexcept {
auto lg = lock();
raw = std::move(val);
return *this;
}
value_t *operator->() {
return &raw;
}
value_t &operator*() {
return raw;
}
const value_t &operator*() const {
return raw;
}
value_t raw;
private:
mutex_t _lock;
};
} // namespace util
#endif //T_MAN_SYNC_H

245
src/task_pool.h Normal file
View File

@@ -0,0 +1,245 @@
#ifndef KITTY_TASK_POOL_H
#define KITTY_TASK_POOL_H
#include <chrono>
#include <deque>
#include <functional>
#include <future>
#include <mutex>
#include <optional>
#include <type_traits>
#include <utility>
#include <vector>
#include "move_by_copy.h"
#include "utility.h"
namespace util {
class _ImplBase {
public:
//_unique_base_type _this_ptr;
inline virtual ~_ImplBase() = default;
virtual void run() = 0;
};
template<class Function>
class _Impl : public _ImplBase {
Function _func;
public:
_Impl(Function &&f) : _func(std::forward<Function>(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 R>
class timer_task_t {
public:
task_id_t task_id;
std::future<R> future;
timer_task_t(task_id_t task_id, std::future<R> &future) : task_id { task_id }, future { std::move(future) } {}
};
protected:
std::deque<__task> _tasks;
std::vector<std::pair<__time_point, __task>> _timer_tasks;
std::mutex _task_mutex;
public:
TaskPool() = default;
TaskPool(TaskPool &&other) noexcept : _tasks { std::move(other._tasks) }, _timer_tasks { std::move(other._timer_tasks) } {}
TaskPool &operator=(TaskPool &&other) noexcept {
std::swap(_tasks, other._tasks);
std::swap(_timer_tasks, other._timer_tasks);
return *this;
}
template<class Function, class... Args>
auto push(Function &&newTask, Args &&...args) {
static_assert(std::is_invocable_v<Function, Args &&...>, "arguments don't match the function");
using __return = std::invoke_result_t<Function, Args &&...>;
using task_t = std::packaged_task<__return()>;
auto bind = [task = std::forward<Function>(newTask), tuple_args = std::make_tuple(std::forward<Args>(args)...)]() mutable {
return std::apply(task, std::move(tuple_args));
};
task_t task(std::move(bind));
auto future = task.get_future();
std::lock_guard<std::mutex> 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<class Function, class X, class Y, class... Args>
auto pushDelayed(Function &&newTask, std::chrono::duration<X, Y> duration, Args &&...args) {
static_assert(std::is_invocable_v<Function, Args &&...>, "arguments don't match the function");
using __return = std::invoke_result_t<Function, Args &&...>;
using task_t = std::packaged_task<__return()>;
__time_point time_point;
if constexpr(std::is_floating_point_v<X>) {
time_point = std::chrono::steady_clock::now() + std::chrono::duration_cast<std::chrono::nanoseconds>(duration);
}
else {
time_point = std::chrono::steady_clock::now() + duration;
}
auto bind = [task = std::forward<Function>(newTask), tuple_args = std::make_tuple(std::forward<Args>(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<class X, class Y>
void delay(task_id_t task_id, std::chrono::duration<X, Y> duration) {
std::lock_guard<std::mutex> lg(_task_mutex);
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;
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;
}
}
bool cancel(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);
if(&*task == task_id) {
_timer_tasks.erase(it);
return true;
}
}
return false;
}
std::optional<std::pair<__time_point, __task>> pop(task_id_t task_id) {
std::lock_guard lg(_task_mutex);
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(pos == std::end(_timer_tasks)) {
return std::nullopt;
}
return std::move(*pos);
}
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);
}
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);
}
return std::nullopt;
}
bool ready() {
std::lock_guard<std::mutex> 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<std::mutex> lg(_task_mutex);
if(_timer_tasks.empty()) {
return std::nullopt;
}
return std::get<0>(_timer_tasks.back());
}
private:
template<class Function>
std::unique_ptr<_ImplBase> toRunnable(Function &&f) {
return std::make_unique<_Impl<Function>>(std::forward<Function &&>(f));
}
};
} // namespace util
#endif

121
src/thread_pool.h Normal file
View File

@@ -0,0 +1,121 @@
#ifndef KITTY_THREAD_POOL_H
#define KITTY_THREAD_POOL_H
#include "task_pool.h"
#include <thread>
namespace util {
/*
* Allow threads to execute unhindered
* while keeping full controll over the threads.
*/
class ThreadPool : public TaskPool {
public:
typedef TaskPool::__task __task;
private:
std::vector<std::thread> _thread;
std::condition_variable _cv;
std::mutex _lock;
bool _continue;
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<class Function, class... Args>
auto push(Function &&newTask, Args &&...args) {
std::lock_guard lg(_lock);
auto future = TaskPool::push(std::forward<Function>(newTask), std::forward<Args>(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<class Function, class X, class Y, class... Args>
auto pushDelayed(Function &&newTask, std::chrono::duration<X, Y> duration, Args &&...args) {
std::lock_guard lg(_lock);
auto future = TaskPool::pushDelayed(std::forward<Function>(newTask), duration, std::forward<Args>(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 {
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();
}
}
};
} // namespace util
#endif

511
src/thread_safe.h Normal file
View File

@@ -0,0 +1,511 @@
//
// Created by loki on 6/10/19.
//
#ifndef SUNSHINE_THREAD_SAFE_H
#define SUNSHINE_THREAD_SAFE_H
#include <atomic>
#include <condition_variable>
#include <functional>
#include <map>
#include <mutex>
#include <vector>
#include "utility.h"
namespace safe {
template<class T>
class event_t {
public:
using status_t = util::optional_t<T>;
template<class... Args>
void raise(Args &&...args) {
std::lock_guard lg { _lock };
if(!_continue) {
return;
}
if constexpr(std::is_same_v<std::optional<T>, status_t>) {
_status = std::make_optional<T>(std::forward<Args>(args)...);
}
else {
_status = status_t { std::forward<Args>(args)... };
}
_cv.notify_all();
}
// pop and view shoud not be used interchangebly
status_t pop() {
std::unique_lock ul { _lock };
if(!_continue) {
return util::false_v<status_t>;
}
while(!_status) {
_cv.wait(ul);
if(!_continue) {
return util::false_v<status_t>;
}
}
auto val = std::move(_status);
_status = util::false_v<status_t>;
return val;
}
// pop and view shoud not be used interchangebly
template<class Rep, class Period>
status_t pop(std::chrono::duration<Rep, Period> delay) {
std::unique_lock ul { _lock };
if(!_continue) {
return util::false_v<status_t>;
}
while(!_status) {
if(!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) {
return util::false_v<status_t>;
}
}
auto val = std::move(_status);
_status = util::false_v<status_t>;
return val;
}
// pop and view shoud not be used interchangebly
const status_t &view() {
std::unique_lock ul { _lock };
if(!_continue) {
return util::false_v<status_t>;
}
while(!_status) {
_cv.wait(ul);
if(!_continue) {
return util::false_v<status_t>;
}
}
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<status_t>;
}
[[nodiscard]] bool running() const {
return _continue;
}
private:
bool _continue { true };
status_t _status { util::false_v<status_t> };
std::condition_variable _cv;
std::mutex _lock;
};
template<class T>
class alarm_raw_t {
public:
using status_t = util::optional_t<T>;
alarm_raw_t() : _status { util::false_v<status_t> } {}
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<class Rep, class Period>
auto wait_for(const std::chrono::duration<Rep, Period> &rel_time) {
std::unique_lock ul(_lock);
return _cv.wait_for(ul, rel_time, [this]() { return (bool)status(); });
}
template<class Rep, class Period, class Pred>
auto wait_for(const std::chrono::duration<Rep, Period> &rel_time, Pred &&pred) {
std::unique_lock ul(_lock);
return _cv.wait_for(ul, rel_time, [this, &pred]() { return (bool)status() || pred(); });
}
template<class Rep, class Period>
auto wait_until(const std::chrono::duration<Rep, Period> &rel_time) {
std::unique_lock ul(_lock);
return _cv.wait_until(ul, rel_time, [this]() { return (bool)status(); });
}
template<class Rep, class Period, class Pred>
auto wait_until(const std::chrono::duration<Rep, Period> &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<class Pred>
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<class T>
using alarm_t = std::shared_ptr<alarm_raw_t<T>>;
template<class T>
alarm_t<T> make_alarm() {
return std::make_shared<alarm_raw_t<T>>();
}
template<class T>
class queue_t {
public:
using status_t = util::optional_t<T>;
queue_t(std::uint32_t max_elements = 32) : _max_elements { max_elements } {}
template<class... Args>
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>(args)...);
_cv.notify_all();
}
bool peek() {
return _continue && !_queue.empty();
}
template<class Rep, class Period>
status_t pop(std::chrono::duration<Rep, Period> delay) {
std::unique_lock ul { _lock };
if(!_continue) {
return util::false_v<status_t>;
}
while(_queue.empty()) {
if(!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) {
return util::false_v<status_t>;
}
}
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<status_t>;
}
while(_queue.empty()) {
_cv.wait(ul);
if(!_continue) {
return util::false_v<status_t>;
}
}
auto val = std::move(_queue.front());
_queue.erase(std::begin(_queue));
return val;
}
std::vector<T> &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<T> _queue;
};
template<class T>
class shared_t {
public:
using element_type = T;
using construct_f = std::function<int(element_type &)>;
using destruct_f = std::function<void(element_type &)>;
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;
}
~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();
}
owner = nullptr;
}
element_type *get() const {
return reinterpret_cast<element_type *>(owner->_object_buf.data());
}
element_type *operator->() {
return reinterpret_cast<element_type *>(owner->_object_buf.data());
}
};
template<class FC, class FD>
shared_t(FC &&fc, FD &&fd) : _construct { std::forward<FC>(fc) }, _destruct { std::forward<FD>(fd) } {}
[[nodiscard]] ptr_t ref() {
std::lock_guard lg { _lock };
if(!_count) {
new(_object_buf.data()) element_type;
if(_construct(*reinterpret_cast<element_type *>(_object_buf.data()))) {
return ptr_t { nullptr };
}
}
++_count;
return ptr_t { this };
}
private:
construct_f _construct;
destruct_f _destruct;
std::array<std::uint8_t, sizeof(element_type)> _object_buf;
std::uint32_t _count;
std::mutex _lock;
};
template<class T, class F_Construct, class F_Destruct>
auto make_shared(F_Construct &&fc, F_Destruct &&fd) {
return shared_t<T> {
std::forward<F_Construct>(fc), std::forward<F_Destruct>(fd)
};
}
using signal_t = event_t<bool>;
class mail_raw_t;
using mail_t = std::shared_ptr<mail_raw_t>;
void cleanup(mail_raw_t *);
template<class T>
class post_t : public T {
public:
template<class... Args>
post_t(mail_t mail, Args &&...args) : T(std::forward<Args>(args)...), mail { std::move(mail) } {}
mail_t mail;
~post_t() {
cleanup(mail.get());
}
};
template<class T>
inline auto lock(const std::weak_ptr<void> &wp) {
return std::reinterpret_pointer_cast<typename T::element_type>(wp.lock());
}
class mail_raw_t : public std::enable_shared_from_this<mail_raw_t> {
public:
template<class T>
using event_t = std::shared_ptr<post_t<event_t<T>>>;
template<class T>
using queue_t = std::shared_ptr<post_t<queue_t<T>>>;
template<class T>
event_t<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<event_t<T>>(it->second);
}
auto post = std::make_shared<typename event_t<T>::element_type>(shared_from_this());
id_to_post.emplace(std::pair<std::string, std::weak_ptr<void>> { std::string { id }, post });
return post;
}
template<class T>
queue_t<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<queue_t<T>>(it->second);
}
auto post = std::make_shared<typename queue_t<T>::element_type>(shared_from_this(), 32);
id_to_post.emplace(std::pair<std::string, std::weak_ptr<void>> { 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::string, std::weak_ptr<void>, std::less<>> id_to_post;
};
inline void cleanup(mail_raw_t *mail) {
mail->cleanup();
}
} // namespace safe
#endif //SUNSHINE_THREAD_SAFE_H

184
src/upnp.cpp Normal file
View File

@@ -0,0 +1,184 @@
#include <miniupnpc/miniupnpc.h>
#include <miniupnpc/upnpcommands.h>
#include "config.h"
#include "confighttp.h"
#include "main.h"
#include "network.h"
#include "nvhttp.h"
#include "rtsp.h"
#include "stream.h"
#include "upnp.h"
#include "utility.h"
using namespace std::literals;
namespace upnp {
constexpr auto INET6_ADDRESS_STRLEN = 46;
constexpr auto IPv4 = 0;
constexpr auto IPv6 = 1;
using device_t = util::safe_ptr<UPNPDev, freeUPNPDevlist>;
KITTY_USING_MOVE_T(urls_t, UPNPUrls, , {
FreeUPNPUrls(&el);
});
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<mapping_t>::const_reverse_iterator begin,
std::vector<mapping_t>::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<mapping_t>::const_reverse_iterator;
deinit_t(urls_t &&urls, IGDdatas data, std::vector<mapping_t> &&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_t> 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 "A UPnP device has been found, but it wasn't recognized as an IGD"sv;
}
return "Unknown status"sv;
}
std::unique_ptr<platf::deinit_t> start() {
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<char, INET6_ADDRESS_STRLEN> lan_addr;
std::array<char, INET6_ADDRESS_STRLEN> 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) {
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();
}
}
if(!config::sunshine.flags[config::flag::UPNP]) {
return nullptr;
}
auto rtsp = std::to_string(map_port(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<mapping_t> 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<deinit_t>(std::move(urls), data, std::move(mappings));
}
} // namespace upnp

10
src/upnp.h Normal file
View File

@@ -0,0 +1,10 @@
#ifndef SUNSHINE_UPNP_H
#define SUNSHINE_UPNP_H
#include "platform/common.h"
namespace upnp {
[[nodiscard]] std::unique_ptr<platf::deinit_t> start();
}
#endif

924
src/utility.h Normal file
View File

@@ -0,0 +1,924 @@
#ifndef UTILITY_H
#define UTILITY_H
#include <algorithm>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <optional>
#include <string_view>
#include <type_traits>
#include <variant>
#include <vector>
#define KITTY_WHILE_LOOP(x, y, z) \
{ \
x; \
while(y) z \
}
template<typename T>
struct argument_type;
template<typename T, typename U>
struct argument_type<T(U)> { typedef U type; };
#define KITTY_USING_MOVE_T(move_t, t, init_val, z) \
class move_t { \
public: \
using element_type = typename argument_type<void(t)>::type; \
\
move_t() : el { init_val } {} \
template<class... Args> \
move_t(Args &&...args) : el { std::forward<Args>(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 &el; } \
const element_type *operator->() const { return &el; } \
\
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 &operator=(x &&) noexcept = default; \
x();
#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 &operator=(x &&) = default; \
x() = 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_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_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<b>(a##_)
#define TUPLE_EL_REF(a, b, expr) \
auto &a = std::get<b>(expr)
namespace util {
template<template<typename...> class X, class... Y>
struct __instantiation_of : public std::false_type {};
template<template<typename...> class X, class... Y>
struct __instantiation_of<X, X<Y...>> : public std::true_type {};
template<template<typename...> class X, class T, class... Y>
static constexpr auto instantiation_of_v = __instantiation_of<X, T, Y...>::value;
template<bool V, class X, class Y>
struct __either;
template<class X, class Y>
struct __either<true, X, Y> {
using type = X;
};
template<class X, class Y>
struct __either<false, X, Y> {
using type = Y;
};
template<bool V, class X, class Y>
using either_t = typename __either<V, X, Y>::type;
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
template<class T>
class FailGuard {
public:
FailGuard() = delete;
FailGuard(T &&f) noexcept : _func { std::forward<T>(f) } {}
FailGuard(FailGuard &&other) noexcept : _func { std::move(other._func) } {
this->failure = other.failure;
other.failure = false;
}
FailGuard(const FailGuard &) = delete;
FailGuard &operator=(const FailGuard &) = delete;
FailGuard &operator=(FailGuard &&other) = delete;
~FailGuard() noexcept {
if(failure) {
_func();
}
}
void disable() { failure = false; }
bool failure { true };
private:
T _func;
};
template<class T>
[[nodiscard]] auto fail_guard(T &&f) {
return FailGuard<T> { std::forward<T>(f) };
}
template<class T>
void append_struct(std::vector<uint8_t> &buf, const T &_struct) {
constexpr size_t data_len = sizeof(_struct);
buf.reserve(data_len);
auto *data = (uint8_t *)&_struct;
for(size_t x = 0; x < data_len; ++x) {
buf.push_back(data[x]);
}
}
template<class T>
class Hex {
public:
typedef T elem_type;
private:
const char _bits[16] {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
char _hex[sizeof(elem_type) * 2];
public:
Hex(const elem_type &elem, bool rev) {
if(!rev) {
const uint8_t *data = reinterpret_cast<const uint8_t *>(&elem) + sizeof(elem_type) - 1;
for(auto it = begin(); it < cend();) {
*it++ = _bits[*data / 16];
*it++ = _bits[*data-- % 16];
}
}
else {
const uint8_t *data = reinterpret_cast<const uint8_t *>(&elem);
for(auto it = begin(); it < cend();) {
*it++ = _bits[*data / 16];
*it++ = _bits[*data++ % 16];
}
}
}
char *begin() { return _hex; }
char *end() { return _hex + sizeof(elem_type) * 2; }
const char *begin() const { return _hex; }
const char *end() const { return _hex + sizeof(elem_type) * 2; }
const char *cbegin() const { return _hex; }
const char *cend() const { return _hex + sizeof(elem_type) * 2; }
std::string to_string() const {
return { begin(), end() };
}
std::string_view to_string_view() const {
return { begin(), sizeof(elem_type) * 2 };
}
};
template<class T>
Hex<T> hex(const T &elem, bool rev = false) {
return Hex<T>(elem, rev);
}
template<class It>
std::string hex_vec(It begin, It end, bool rev = false) {
auto str_size = 2 * std::distance(begin, end);
std::string hex;
hex.resize(str_size);
const char _bits[16] {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
if(rev) {
for(auto it = std::begin(hex); it < std::end(hex);) {
*it++ = _bits[((uint8_t)*begin) / 16];
*it++ = _bits[((uint8_t)*begin++) % 16];
}
}
else {
--end;
for(auto it = std::begin(hex); it < std::end(hex);) {
*it++ = _bits[((uint8_t)*end) / 16];
*it++ = _bits[((uint8_t)*end--) % 16];
}
}
return hex;
}
template<class C>
std::string hex_vec(C &&c, bool rev = false) {
return hex_vec(std::begin(c), std::end(c), rev);
}
template<class T>
T from_hex(const std::string_view &hex, bool rev = false) {
std::uint8_t buf[sizeof(T)];
static char constexpr shift_bit = 'a' - 'A';
auto is_convertable = [](char ch) -> bool {
if(isdigit(ch)) {
return true;
}
ch |= shift_bit;
if('a' > ch || ch > 'z') {
return false;
}
return true;
};
auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2;
auto padding = sizeof(T) - buf_size;
const char *data = hex.data() + hex.size() - 1;
auto convert = [](char ch) -> std::uint8_t {
if(ch >= '0' && ch <= '9') {
return (std::uint8_t)ch - '0';
}
return (std::uint8_t)(ch | (char)32) - 'a' + (char)10;
};
std::fill_n(buf + buf_size, padding, 0);
std::for_each_n(buf, buf_size, [&](auto &el) {
while(!is_convertable(*data)) { --data; }
std::uint8_t ch_r = convert(*data--);
while(!is_convertable(*data)) { --data; }
std::uint8_t ch_l = convert(*data--);
el = (ch_l << 4) | ch_r;
});
if(rev) {
std::reverse(std::begin(buf), std::end(buf));
}
return *reinterpret_cast<T *>(buf);
}
inline std::string from_hex_vec(const std::string &hex, bool rev = false) {
std::string buf;
static char constexpr shift_bit = 'a' - 'A';
auto is_convertable = [](char ch) -> bool {
if(isdigit(ch)) {
return true;
}
ch |= shift_bit;
if('a' > ch || ch > 'z') {
return false;
}
return true;
};
auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2;
buf.resize(buf_size);
const char *data = hex.data() + hex.size() - 1;
auto convert = [](char ch) -> std::uint8_t {
if(ch >= '0' && ch <= '9') {
return (std::uint8_t)ch - '0';
}
return (std::uint8_t)(ch | (char)32) - 'a' + (char)10;
};
for(auto &el : buf) {
while(!is_convertable(*data)) { --data; }
std::uint8_t ch_r = convert(*data--);
while(!is_convertable(*data)) { --data; }
std::uint8_t ch_l = convert(*data--);
el = (ch_l << 4) | ch_r;
}
if(rev) {
std::reverse(std::begin(buf), std::end(buf));
}
return buf;
}
template<class T>
class hash {
public:
using value_type = T;
std::size_t operator()(const value_type &value) const {
const auto *p = reinterpret_cast<const char *>(&value);
return std::hash<std::string_view> {}(std::string_view { p, sizeof(value_type) });
}
};
template<class T>
auto enm(const T &val) -> const std::underlying_type_t<T> & {
return *reinterpret_cast<const std::underlying_type_t<T> *>(&val);
}
template<class T>
auto enm(T &val) -> std::underlying_type_t<T> & {
return *reinterpret_cast<std::underlying_type_t<T> *>(&val);
}
inline std::int64_t from_chars(const char *begin, const char *end) {
if(begin == end) {
return 0;
}
std::int64_t res {};
std::int64_t mul = 1;
while(begin != --end) {
res += (std::int64_t)(*end - '0') * mul;
mul *= 10;
}
return *begin != '-' ? res + (std::int64_t)(*begin - '0') * mul : -res;
}
inline std::int64_t from_view(const std::string_view &number) {
return from_chars(std::begin(number), std::end(number));
}
template<class X, class Y>
class Either : public std::variant<std::monostate, X, Y> {
public:
using std::variant<std::monostate, X, Y>::variant;
constexpr bool has_left() const {
return std::holds_alternative<X>(*this);
}
constexpr bool has_right() const {
return std::holds_alternative<Y>(*this);
}
X &left() {
return std::get<X>(*this);
}
Y &right() {
return std::get<Y>(*this);
}
const X &left() const {
return std::get<X>(*this);
}
const Y &right() const {
return std::get<Y>(*this);
}
};
// Compared to std::unique_ptr, it adds the ability to get the address of the pointer itself
template<typename T, typename D = std::default_delete<T>>
class uniq_ptr {
public:
using element_type = T;
using pointer = element_type *;
using deleter_type = D;
constexpr uniq_ptr() noexcept : _p { nullptr } {}
constexpr uniq_ptr(std::nullptr_t) noexcept : _p { nullptr } {}
uniq_ptr(const uniq_ptr &other) noexcept = delete;
uniq_ptr &operator=(const uniq_ptr &other) noexcept = delete;
template<class V>
uniq_ptr(V *p) noexcept : _p { p } {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<element_type, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
}
template<class V>
uniq_ptr(std::unique_ptr<V, deleter_type> &&uniq) noexcept : _p { uniq.release() } {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
}
template<class V>
uniq_ptr(uniq_ptr<V, deleter_type> &&other) noexcept : _p { other.release() } {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
}
template<class V>
uniq_ptr &operator=(uniq_ptr<V, deleter_type> &&other) noexcept {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
reset(other.release());
return *this;
}
template<class V>
uniq_ptr &operator=(std::unique_ptr<V, deleter_type> &&uniq) noexcept {
static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, "element_type must be base class of V");
reset(uniq.release());
return *this;
}
~uniq_ptr() {
reset();
}
void reset(pointer p = pointer()) {
if(_p) {
_deleter(_p);
}
_p = p;
}
pointer release() {
auto tmp = _p;
_p = nullptr;
return tmp;
}
pointer get() {
return _p;
}
const pointer get() const {
return _p;
}
const std::add_lvalue_reference_t<element_type> operator*() const {
return *_p;
}
std::add_lvalue_reference_t<element_type> operator*() {
return *_p;
}
const pointer operator->() const {
return _p;
}
pointer operator->() {
return _p;
}
pointer *operator&() const {
return &_p;
}
pointer *operator&() {
return &_p;
}
deleter_type &get_deleter() {
return _deleter;
}
const deleter_type &get_deleter() const {
return _deleter;
}
explicit operator bool() const {
return _p != nullptr;
}
protected:
pointer _p;
deleter_type _deleter;
};
template<class T1, class D1, class T2, class D2>
bool operator==(const uniq_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
return x.get() == y.get();
}
template<class T1, class D1, class T2, class D2>
bool operator!=(const uniq_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
return x.get() != y.get();
}
template<class T1, class D1, class T2, class D2>
bool operator==(const std::unique_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
return x.get() == y.get();
}
template<class T1, class D1, class T2, class D2>
bool operator!=(const std::unique_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {
return x.get() != y.get();
}
template<class T1, class D1, class T2, class D2>
bool operator==(const uniq_ptr<T1, D1> &x, const std::unique_ptr<T1, D1> &y) {
return x.get() == y.get();
}
template<class T1, class D1, class T2, class D2>
bool operator!=(const uniq_ptr<T1, D1> &x, const std::unique_ptr<T1, D1> &y) {
return x.get() != y.get();
}
template<class T, class D>
bool operator==(const uniq_ptr<T, D> &x, std::nullptr_t) {
return !(bool)x;
}
template<class T, class D>
bool operator!=(const uniq_ptr<T, D> &x, std::nullptr_t) {
return (bool)x;
}
template<class T, class D>
bool operator==(std::nullptr_t, const uniq_ptr<T, D> &y) {
return !(bool)y;
}
template<class T, class D>
bool operator!=(std::nullptr_t, const uniq_ptr<T, D> &y) {
return (bool)y;
}
template<class P>
using shared_t = std::shared_ptr<typename P::element_type>;
template<class P, class T>
shared_t<P> make_shared(T *pointer) {
return shared_t<P>(reinterpret_cast<typename P::pointer>(pointer), typename P::deleter_type());
}
template<class T>
class wrap_ptr {
public:
using element_type = T;
using pointer = element_type *;
using reference = element_type &;
wrap_ptr() : _own_ptr { false }, _p { nullptr } {}
wrap_ptr(pointer p) : _own_ptr { false }, _p { p } {}
wrap_ptr(std::unique_ptr<element_type> &&uniq_p) : _own_ptr { true }, _p { uniq_p.release() } {}
wrap_ptr(wrap_ptr &&other) : _own_ptr { other._own_ptr }, _p { other._p } {
other._own_ptr = false;
}
wrap_ptr &operator=(wrap_ptr &&other) noexcept {
if(_own_ptr) {
delete _p;
}
_p = other._p;
_own_ptr = other._own_ptr;
other._own_ptr = false;
return *this;
}
template<class V>
wrap_ptr &operator=(std::unique_ptr<V> &&uniq_ptr) {
static_assert(std::is_base_of_v<element_type, V>, "element_type must be base class of V");
_own_ptr = true;
_p = uniq_ptr.release();
return *this;
}
wrap_ptr &operator=(pointer p) {
if(_own_ptr) {
delete _p;
}
_p = p;
_own_ptr = false;
return *this;
}
~wrap_ptr() {
if(_own_ptr) {
delete _p;
}
_own_ptr = false;
}
const reference operator*() const {
return *_p;
}
reference operator*() {
return *_p;
}
const pointer operator->() const {
return _p;
}
pointer operator->() {
return _p;
}
private:
bool _own_ptr;
pointer _p;
};
template<class T>
constexpr bool is_pointer_v =
instantiation_of_v<std::unique_ptr, T> ||
instantiation_of_v<std::shared_ptr, T> ||
instantiation_of_v<uniq_ptr, T> ||
std::is_pointer_v<T>;
template<class T, class V = void>
struct __false_v;
template<class T>
struct __false_v<T, std::enable_if_t<instantiation_of_v<std::optional, T>>> {
static constexpr std::nullopt_t value = std::nullopt;
};
template<class T>
struct __false_v<T, std::enable_if_t<is_pointer_v<T>>> {
static constexpr std::nullptr_t value = nullptr;
};
template<class T>
struct __false_v<T, std::enable_if_t<std::is_same_v<T, bool>>> {
static constexpr bool value = false;
};
template<class T>
static constexpr auto false_v = __false_v<T>::value;
template<class T>
using optional_t = either_t<
(std::is_same_v<T, bool> || is_pointer_v<T>),
T, std::optional<T>>;
template<class T>
class buffer_t {
public:
buffer_t() : _els { 0 } {};
buffer_t(buffer_t &&o) noexcept : _els { o._els }, _buf { std::move(o._buf) } {
o._els = 0;
}
buffer_t &operator=(buffer_t &&o) noexcept {
std::swap(_els, o._els);
std::swap(_buf, o._buf);
return *this;
};
explicit buffer_t(size_t elements) : _els { elements }, _buf { std::make_unique<T[]>(elements) } {}
explicit buffer_t(size_t elements, const T &t) : _els { elements }, _buf { std::make_unique<T[]>(elements) } {
std::fill_n(_buf.get(), elements, t);
}
T &operator[](size_t el) {
return _buf[el];
}
const T &operator[](size_t el) const {
return _buf[el];
}
size_t size() const {
return _els;
}
void fake_resize(std::size_t els) {
_els = els;
}
T *begin() {
return _buf.get();
}
const T *begin() const {
return _buf.get();
}
T *end() {
return _buf.get() + _els;
}
const T *end() const {
return _buf.get() + _els;
}
private:
size_t _els;
std::unique_ptr<T[]> _buf;
};
template<class T>
T either(std::optional<T> &&l, T &&r) {
if(l) {
return std::move(*l);
}
return std::forward<T>(r);
}
template<class ReturnType, class... Args>
struct Function {
typedef ReturnType (*type)(Args...);
};
template<class T, class ReturnType, typename Function<ReturnType, T>::type function>
struct Destroy {
typedef T pointer;
void operator()(pointer p) {
function(p);
}
};
template<class T, typename Function<void, T *>::type function>
using safe_ptr = uniq_ptr<T, Destroy<T *, void, function>>;
// You cannot specialize an alias
template<class T, class ReturnType, typename Function<ReturnType, T *>::type function>
using safe_ptr_v2 = uniq_ptr<T, Destroy<T *, ReturnType, function>>;
template<class T>
void c_free(T *p) {
free(p);
}
template<class T, class ReturnType, ReturnType (**function)(T *)>
void dynamic(T *p) {
(*function)(p);
}
template<class T, void (**function)(T *)>
using dyn_safe_ptr = safe_ptr<T, dynamic<T, void, function>>;
template<class T, class ReturnType, ReturnType (**function)(T *)>
using dyn_safe_ptr_v2 = safe_ptr<T, dynamic<T, ReturnType, function>>;
template<class T>
using c_ptr = safe_ptr<T, c_free<T>>;
template<class It>
std::string_view view(It begin, It end) {
return std::string_view { (const char *)begin, (std::size_t)(end - begin) };
}
template<class T>
std::string_view view(const T &data) {
return std::string_view((const char *)&data, sizeof(T));
}
namespace endian {
template<class T = void>
struct endianness {
enum : bool {
#if defined(__BYTE_ORDER) && __BYTE_ORDER == __BIG_ENDIAN || \
defined(__BIG_ENDIAN__) || \
defined(__ARMEB__) || \
defined(__THUMBEB__) || \
defined(__AARCH64EB__) || \
defined(_MIBSEB) || defined(__MIBSEB) || defined(__MIBSEB__)
// It's a big-endian target architecture
little = false,
#elif defined(__BYTE_ORDER) && __BYTE_ORDER == __LITTLE_ENDIAN || \
defined(__LITTLE_ENDIAN__) || \
defined(__ARMEL__) || \
defined(__THUMBEL__) || \
defined(__AARCH64EL__) || \
defined(_MIPSEL) || defined(__MIPSEL) || defined(__MIPSEL__) || \
defined(_WIN32)
// It's a little-endian target architecture
little = true,
#else
#error "Unknown Endianness"
#endif
big = !little
};
};
template<class T, class S = void>
struct endian_helper {};
template<class T>
struct endian_helper<T, std::enable_if_t<
!(instantiation_of_v<std::optional, T>)>> {
static inline T big(T x) {
if constexpr(endianness<T>::little) {
uint8_t *data = reinterpret_cast<uint8_t *>(&x);
std::reverse(data, data + sizeof(x));
}
return x;
}
static inline T little(T x) {
if constexpr(endianness<T>::big) {
uint8_t *data = reinterpret_cast<uint8_t *>(&x);
std::reverse(data, data + sizeof(x));
}
return x;
}
};
template<class T>
struct endian_helper<T, std::enable_if_t<
instantiation_of_v<std::optional, T>>> {
static inline T little(T x) {
if(!x) return x;
if constexpr(endianness<T>::big) {
auto *data = reinterpret_cast<uint8_t *>(&*x);
std::reverse(data, data + sizeof(*x));
}
return x;
}
static inline T big(T x) {
if(!x) return x;
if constexpr(endianness<T>::little) {
auto *data = reinterpret_cast<uint8_t *>(&*x);
std::reverse(data, data + sizeof(*x));
}
return x;
}
};
template<class T>
inline auto little(T x) { return endian_helper<T>::little(x); }
template<class T>
inline auto big(T x) { return endian_helper<T>::big(x); }
} // namespace endian
} // namespace util
#endif

79
src/uuid.h Normal file
View File

@@ -0,0 +1,79 @@
//
// Created by loki on 8-2-19.
//
#ifndef T_MAN_UUID_H
#define T_MAN_UUID_H
#include <random>
namespace util {
union uuid_t {
std::uint8_t b8[16];
std::uint16_t b16[8];
std::uint32_t b32[4];
std::uint64_t b64[2];
static uuid_t generate(std::default_random_engine &engine) {
std::uniform_int_distribution<std::uint8_t> dist(0, std::numeric_limits<std::uint8_t>::max());
uuid_t buf;
for(auto &el : buf.b8) {
el = dist(engine);
}
buf.b8[7] &= (std::uint8_t)0b00101111;
buf.b8[9] &= (std::uint8_t)0b10011111;
return buf;
}
static uuid_t generate() {
std::random_device r;
std::default_random_engine engine { r() };
return generate(engine);
}
[[nodiscard]] std::string string() const {
std::string result;
result.reserve(sizeof(uuid_t) * 2 + 4);
auto hex = util::hex(*this, true);
auto hex_view = hex.to_string_view();
std::string_view slices[] = {
hex_view.substr(0, 8),
hex_view.substr(8, 4),
hex_view.substr(12, 4),
hex_view.substr(16, 4)
};
auto last_slice = hex_view.substr(20, 12);
for(auto &slice : slices) {
std::copy(std::begin(slice), std::end(slice), std::back_inserter(result));
result.push_back('-');
}
std::copy(std::begin(last_slice), std::end(last_slice), std::back_inserter(result));
return result;
}
constexpr bool operator==(const uuid_t &other) const {
return b64[0] == other.b64[0] && b64[1] == other.b64[1];
}
constexpr bool operator<(const uuid_t &other) const {
return (b64[0] < other.b64[0] || (b64[0] == other.b64[0] && b64[1] < other.b64[1]));
}
constexpr bool operator>(const uuid_t &other) const {
return (b64[0] > other.b64[0] || (b64[0] == other.b64[0] && b64[1] > other.b64[1]));
}
};
} // namespace util
#endif //T_MAN_UUID_H

1874
src/video.cpp Normal file

File diff suppressed because it is too large Load Diff

87
src/video.h Normal file
View File

@@ -0,0 +1,87 @@
//
// Created by loki on 6/9/19.
//
#ifndef SUNSHINE_VIDEO_H
#define SUNSHINE_VIDEO_H
#include "input.h"
#include "platform/common.h"
#include "thread_safe.h"
extern "C" {
#include <libavcodec/avcodec.h>
}
struct AVPacket;
namespace video {
struct packet_raw_t {
void init_packet() {
this->av_packet = av_packet_alloc();
}
template<class P>
explicit packet_raw_t(P *user_data) : channel_data { user_data } {
init_packet();
}
explicit packet_raw_t(std::nullptr_t) : channel_data { nullptr } {
init_packet();
}
~packet_raw_t() {
av_packet_unref(this->av_packet);
}
struct replace_t {
std::string_view old;
std::string_view _new;
KITTY_DEFAULT_CONSTR_MOVE(replace_t)
replace_t(std::string_view old, std::string_view _new) noexcept : old { std::move(old) }, _new { std::move(_new) } {}
};
AVPacket *av_packet;
std::vector<replace_t> *replacements;
void *channel_data;
};
using packet_t = std::unique_ptr<packet_raw_t>;
struct config_t {
int width;
int height;
int framerate;
int bitrate;
int slicesPerFrame;
int numRefFrames;
int encoderCscMode;
int videoFormat;
int dynamicRange;
};
using float4 = float[4];
using float3 = float[3];
using float2 = float[2];
struct __attribute__((__aligned__(16))) color_t {
float4 color_vec_y;
float4 color_vec_u;
float4 color_vec_v;
float2 range_y;
float2 range_uv;
};
extern color_t colors[4];
void capture(
safe::mail_t mail,
config_t config,
void *channel_data);
int init();
} // namespace video
#endif //SUNSHINE_VIDEO_H