Apply clang-format across the whole tree
Run clang-format (the repo's .clang-format: LLVM base, 120 cols, tabs, Allman functions) over every source file so the tree is formatter-clean. Whitespace only -- no behavior change; full x64 + x86 suites pass. Also set SortIncludes: false in .clang-format. Windows include order is load-bearing (windows.h must precede tlhelp32.h / mmreg.h / xinput.h / dinput.h; winsock2.h must precede windows.h), and the default alphabetical sort reorders tlhelp32.h ahead of windows.h -- a build break. Leaving order alone keeps the manual, correct grouping.
This commit is contained in:
@@ -16,4 +16,9 @@ AlwaysBreakTemplateDeclarations: true
|
|||||||
BreakBeforeBinaryOperators: NonAssignment
|
BreakBeforeBinaryOperators: NonAssignment
|
||||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||||
PointerAlignment: Left
|
PointerAlignment: Left
|
||||||
|
|
||||||
|
# Windows include order is load-bearing (windows.h must precede tlhelp32.h / mmreg.h /
|
||||||
|
# xinput.h / dinput.h; winsock2.h must precede windows.h), so leave include order alone
|
||||||
|
# rather than let an alphabetical sort break the build.
|
||||||
|
SortIncludes: false
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -24,8 +24,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// The standard sample rates a shared-mode WASAPI stream realistically uses. Candidates are this
|
// The standard sample rates a shared-mode WASAPI stream realistically uses. Candidates are this
|
||||||
// set; a non-standard true rate is out of scope (and would show as low-confidence either way).
|
// set; a non-standard true rate is out of scope (and would show as low-confidence either way).
|
||||||
@@ -35,30 +34,25 @@ inline const std::vector<unsigned>& standard_audio_rates()
|
|||||||
return rates;
|
return rates;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RateCorrelation
|
struct RateCorrelation {
|
||||||
{
|
|
||||||
bool ok = false; // a confident pick was made (winner clears the threshold AND beats the runner-up)
|
bool ok = false; // a confident pick was made (winner clears the threshold AND beats the runner-up)
|
||||||
unsigned rate = 0; // best candidate rate (Hz)
|
unsigned rate = 0; // best candidate rate (Hz)
|
||||||
double score = 0.0; // alignment score of the winner, in [0,1] (1 = perfect)
|
double score = 0.0; // alignment score of the winner, in [0,1] (1 = perfect)
|
||||||
double runner_up = 0.0; // score of the second-best candidate (for separation)
|
double runner_up = 0.0; // score of the second-best candidate (for separation)
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace correlate_detail
|
namespace correlate_detail {
|
||||||
{
|
|
||||||
|
|
||||||
// Average interleaved float frames down to a single mono channel.
|
// Average interleaved float frames down to a single mono channel.
|
||||||
inline void downmix(const float* interleaved, std::size_t frames, unsigned channels, std::vector<float>& out)
|
inline void downmix(const float* interleaved, std::size_t frames, unsigned channels, std::vector<float>& out)
|
||||||
{
|
{
|
||||||
out.resize(frames);
|
out.resize(frames);
|
||||||
if (channels == 0)
|
if (channels == 0) {
|
||||||
{
|
|
||||||
channels = 1;
|
channels = 1;
|
||||||
}
|
}
|
||||||
for (std::size_t i = 0; i < frames; ++i)
|
for (std::size_t i = 0; i < frames; ++i) {
|
||||||
{
|
|
||||||
float sum = 0.0f;
|
float sum = 0.0f;
|
||||||
for (unsigned c = 0; c < channels; ++c)
|
for (unsigned c = 0; c < channels; ++c) {
|
||||||
{
|
|
||||||
sum += interleaved[i * channels + c];
|
sum += interleaved[i * channels + c];
|
||||||
}
|
}
|
||||||
out[i] = sum / static_cast<float>(channels);
|
out[i] = sum / static_cast<float>(channels);
|
||||||
@@ -66,24 +60,20 @@ inline void downmix(const float* interleaved, std::size_t frames, unsigned chann
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Linear-resample a mono signal from src_rate to dst_rate.
|
// Linear-resample a mono signal from src_rate to dst_rate.
|
||||||
inline void resample_linear(const std::vector<float>& in, unsigned src_rate, unsigned dst_rate,
|
inline void resample_linear(const std::vector<float>& in, unsigned src_rate, unsigned dst_rate, std::vector<float>& out)
|
||||||
std::vector<float>& out)
|
|
||||||
{
|
|
||||||
if (src_rate == 0 || dst_rate == 0 || in.empty())
|
|
||||||
{
|
{
|
||||||
|
if (src_rate == 0 || dst_rate == 0 || in.empty()) {
|
||||||
out.clear();
|
out.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (src_rate == dst_rate)
|
if (src_rate == dst_rate) {
|
||||||
{
|
|
||||||
out = in;
|
out = in;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const double step = static_cast<double>(src_rate) / static_cast<double>(dst_rate);
|
const double step = static_cast<double>(src_rate) / static_cast<double>(dst_rate);
|
||||||
const std::size_t out_n = static_cast<std::size_t>(static_cast<double>(in.size()) / step);
|
const std::size_t out_n = static_cast<std::size_t>(static_cast<double>(in.size()) / step);
|
||||||
out.resize(out_n);
|
out.resize(out_n);
|
||||||
for (std::size_t i = 0; i < out_n; ++i)
|
for (std::size_t i = 0; i < out_n; ++i) {
|
||||||
{
|
|
||||||
const double pos = static_cast<double>(i) * step;
|
const double pos = static_cast<double>(i) * step;
|
||||||
const std::size_t j = static_cast<std::size_t>(pos);
|
const std::size_t j = static_cast<std::size_t>(pos);
|
||||||
const double frac = pos - static_cast<double>(j);
|
const double frac = pos - static_cast<double>(j);
|
||||||
@@ -97,29 +87,24 @@ inline void resample_linear(const std::vector<float>& in, unsigned src_rate, uns
|
|||||||
// alignment search (the envelope/content alignment doesn't need full bandwidth).
|
// alignment search (the envelope/content alignment doesn't need full bandwidth).
|
||||||
inline void decimate(const std::vector<float>& in, unsigned rate, unsigned corr_rate, std::vector<float>& out)
|
inline void decimate(const std::vector<float>& in, unsigned rate, unsigned corr_rate, std::vector<float>& out)
|
||||||
{
|
{
|
||||||
if (rate <= corr_rate || in.empty())
|
if (rate <= corr_rate || in.empty()) {
|
||||||
{
|
|
||||||
out = in;
|
out = in;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const double factor = static_cast<double>(rate) / static_cast<double>(corr_rate);
|
const double factor = static_cast<double>(rate) / static_cast<double>(corr_rate);
|
||||||
const std::size_t out_n = static_cast<std::size_t>(static_cast<double>(in.size()) / factor);
|
const std::size_t out_n = static_cast<std::size_t>(static_cast<double>(in.size()) / factor);
|
||||||
out.resize(out_n);
|
out.resize(out_n);
|
||||||
for (std::size_t i = 0; i < out_n; ++i)
|
for (std::size_t i = 0; i < out_n; ++i) {
|
||||||
{
|
|
||||||
const std::size_t lo = static_cast<std::size_t>(static_cast<double>(i) * factor);
|
const std::size_t lo = static_cast<std::size_t>(static_cast<double>(i) * factor);
|
||||||
std::size_t hi = static_cast<std::size_t>(static_cast<double>(i + 1) * factor);
|
std::size_t hi = static_cast<std::size_t>(static_cast<double>(i + 1) * factor);
|
||||||
if (hi <= lo)
|
if (hi <= lo) {
|
||||||
{
|
|
||||||
hi = lo + 1;
|
hi = lo + 1;
|
||||||
}
|
}
|
||||||
if (hi > in.size())
|
if (hi > in.size()) {
|
||||||
{
|
|
||||||
hi = in.size();
|
hi = in.size();
|
||||||
}
|
}
|
||||||
float sum = 0.0f;
|
float sum = 0.0f;
|
||||||
for (std::size_t k = lo; k < hi; ++k)
|
for (std::size_t k = lo; k < hi; ++k) {
|
||||||
{
|
|
||||||
sum += in[k];
|
sum += in[k];
|
||||||
}
|
}
|
||||||
out[i] = sum / static_cast<float>(hi - lo);
|
out[i] = sum / static_cast<float>(hi - lo);
|
||||||
@@ -133,29 +118,24 @@ inline double ncc(const std::vector<float>& a, const std::vector<float>& b, long
|
|||||||
{
|
{
|
||||||
double sa = 0.0, sb = 0.0;
|
double sa = 0.0, sb = 0.0;
|
||||||
std::size_t n = 0;
|
std::size_t n = 0;
|
||||||
for (std::size_t i = start; i < start + len && i < a.size(); ++i)
|
for (std::size_t i = start; i < start + len && i < a.size(); ++i) {
|
||||||
{
|
|
||||||
const long bi = static_cast<long>(i) + lag;
|
const long bi = static_cast<long>(i) + lag;
|
||||||
if (bi < 0 || static_cast<std::size_t>(bi) >= b.size())
|
if (bi < 0 || static_cast<std::size_t>(bi) >= b.size()) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
sa += a[i];
|
sa += a[i];
|
||||||
sb += b[bi];
|
sb += b[bi];
|
||||||
++n;
|
++n;
|
||||||
}
|
}
|
||||||
if (n < 8)
|
if (n < 8) {
|
||||||
{
|
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
const double ma = sa / static_cast<double>(n);
|
const double ma = sa / static_cast<double>(n);
|
||||||
const double mb = sb / static_cast<double>(n);
|
const double mb = sb / static_cast<double>(n);
|
||||||
double num = 0.0, da = 0.0, db = 0.0;
|
double num = 0.0, da = 0.0, db = 0.0;
|
||||||
for (std::size_t i = start; i < start + len && i < a.size(); ++i)
|
for (std::size_t i = start; i < start + len && i < a.size(); ++i) {
|
||||||
{
|
|
||||||
const long bi = static_cast<long>(i) + lag;
|
const long bi = static_cast<long>(i) + lag;
|
||||||
if (bi < 0 || static_cast<std::size_t>(bi) >= b.size())
|
if (bi < 0 || static_cast<std::size_t>(bi) >= b.size()) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const double xa = a[i] - ma;
|
const double xa = a[i] - ma;
|
||||||
@@ -164,8 +144,7 @@ inline double ncc(const std::vector<float>& a, const std::vector<float>& b, long
|
|||||||
da += xa * xa;
|
da += xa * xa;
|
||||||
db += xb * xb;
|
db += xb * xb;
|
||||||
}
|
}
|
||||||
if (da < 1e-9 || db < 1e-9)
|
if (da < 1e-9 || db < 1e-9) {
|
||||||
{
|
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
return num / std::sqrt(da * db);
|
return num / std::sqrt(da * db);
|
||||||
@@ -188,11 +167,9 @@ inline double aligned_score(const std::vector<float>& a, const std::vector<float
|
|||||||
const std::size_t mid_len = n / 2;
|
const std::size_t mid_len = n / 2;
|
||||||
double best = -2.0;
|
double best = -2.0;
|
||||||
long best_lag = 0;
|
long best_lag = 0;
|
||||||
for (long lag = -max_lag; lag <= max_lag; ++lag)
|
for (long lag = -max_lag; lag <= max_lag; ++lag) {
|
||||||
{
|
|
||||||
const double c = ncc(a, b, lag, mid_start, mid_len);
|
const double c = ncc(a, b, lag, mid_start, mid_len);
|
||||||
if (c > best)
|
if (c > best) {
|
||||||
{
|
|
||||||
best = c;
|
best = c;
|
||||||
best_lag = lag;
|
best_lag = lag;
|
||||||
}
|
}
|
||||||
@@ -218,8 +195,7 @@ inline RateCorrelation correlate_rate(const std::vector<float>& hook_mono, const
|
|||||||
{
|
{
|
||||||
using namespace correlate_detail;
|
using namespace correlate_detail;
|
||||||
RateCorrelation result;
|
RateCorrelation result;
|
||||||
if (hook_mono.empty() || loop_mono.empty() || device_rate == 0)
|
if (hook_mono.empty() || loop_mono.empty() || device_rate == 0) {
|
||||||
{
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
constexpr unsigned kCorrRate = 8000; // alignment search rate (Nyquist 4 kHz -- plenty for content)
|
constexpr unsigned kCorrRate = 8000; // alignment search rate (Nyquist 4 kHz -- plenty for content)
|
||||||
@@ -230,19 +206,15 @@ inline RateCorrelation correlate_rate(const std::vector<float>& hook_mono, const
|
|||||||
double best = -1.0, second = -1.0;
|
double best = -1.0, second = -1.0;
|
||||||
unsigned best_rate = 0;
|
unsigned best_rate = 0;
|
||||||
std::vector<float> resampled, hook_ds;
|
std::vector<float> resampled, hook_ds;
|
||||||
for (unsigned cand : candidates)
|
for (unsigned cand : candidates) {
|
||||||
{
|
|
||||||
resample_linear(hook_mono, cand, device_rate, resampled); // treat hook as sampled at `cand`
|
resample_linear(hook_mono, cand, device_rate, resampled); // treat hook as sampled at `cand`
|
||||||
decimate(resampled, device_rate, kCorrRate, hook_ds);
|
decimate(resampled, device_rate, kCorrRate, hook_ds);
|
||||||
const double s = aligned_score(hook_ds, loop_ds, kCorrRate);
|
const double s = aligned_score(hook_ds, loop_ds, kCorrRate);
|
||||||
if (s > best)
|
if (s > best) {
|
||||||
{
|
|
||||||
second = best;
|
second = best;
|
||||||
best = s;
|
best = s;
|
||||||
best_rate = cand;
|
best_rate = cand;
|
||||||
}
|
} else if (s > second) {
|
||||||
else if (s > second)
|
|
||||||
{
|
|
||||||
second = s;
|
second = s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,7 +222,8 @@ inline RateCorrelation correlate_rate(const std::vector<float>& hook_mono, const
|
|||||||
result.rate = best_rate;
|
result.rate = best_rate;
|
||||||
result.score = best < 0.0 ? 0.0 : best;
|
result.score = best < 0.0 ? 0.0 : best;
|
||||||
result.runner_up = second < 0.0 ? 0.0 : second;
|
result.runner_up = second < 0.0 ? 0.0 : second;
|
||||||
result.ok = result.score >= min_score && (result.runner_up <= 1e-6 || result.score >= result.runner_up * separation);
|
result.ok =
|
||||||
|
result.score >= min_score && (result.runner_up <= 1e-6 || result.score >= result.runner_up * separation);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,8 +239,7 @@ inline RateCorrelation correlate_rate(const std::vector<float>& hook_mono, const
|
|||||||
inline constexpr unsigned kWaveFormatPcm = 1; // WAVE_FORMAT_PCM
|
inline constexpr unsigned kWaveFormatPcm = 1; // WAVE_FORMAT_PCM
|
||||||
inline constexpr unsigned kWaveFormatFloat = 3; // WAVE_FORMAT_IEEE_FLOAT
|
inline constexpr unsigned kWaveFormatFloat = 3; // WAVE_FORMAT_IEEE_FLOAT
|
||||||
|
|
||||||
struct LayoutCandidate
|
struct LayoutCandidate {
|
||||||
{
|
|
||||||
unsigned channels;
|
unsigned channels;
|
||||||
unsigned bits;
|
unsigned bits;
|
||||||
unsigned tag; // kWaveFormatPcm / kWaveFormatFloat
|
unsigned tag; // kWaveFormatPcm / kWaveFormatFloat
|
||||||
@@ -278,16 +250,14 @@ struct LayoutCandidate
|
|||||||
inline const std::vector<LayoutCandidate>& standard_audio_layouts()
|
inline const std::vector<LayoutCandidate>& standard_audio_layouts()
|
||||||
{
|
{
|
||||||
static const std::vector<LayoutCandidate> v = {
|
static const std::vector<LayoutCandidate> v = {
|
||||||
{2, 32, kWaveFormatFloat}, {1, 32, kWaveFormatFloat}, {6, 32, kWaveFormatFloat},
|
{2, 32, kWaveFormatFloat}, {1, 32, kWaveFormatFloat}, {6, 32, kWaveFormatFloat}, {8, 32, kWaveFormatFloat},
|
||||||
{8, 32, kWaveFormatFloat}, {4, 32, kWaveFormatFloat}, {2, 16, kWaveFormatPcm},
|
{4, 32, kWaveFormatFloat}, {2, 16, kWaveFormatPcm}, {1, 16, kWaveFormatPcm}, {6, 16, kWaveFormatPcm},
|
||||||
{1, 16, kWaveFormatPcm}, {6, 16, kWaveFormatPcm}, {8, 16, kWaveFormatPcm},
|
{8, 16, kWaveFormatPcm}, {4, 16, kWaveFormatPcm},
|
||||||
{4, 16, kWaveFormatPcm},
|
|
||||||
};
|
};
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FormatCorrelation
|
struct FormatCorrelation {
|
||||||
{
|
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
unsigned rate = 0;
|
unsigned rate = 0;
|
||||||
unsigned channels = 0;
|
unsigned channels = 0;
|
||||||
@@ -302,15 +272,13 @@ struct FormatCorrelation
|
|||||||
// padding is stale staging-buffer bytes, so the host must extract the real `count*real_block` bytes
|
// padding is stale staging-buffer bytes, so the host must extract the real `count*real_block` bytes
|
||||||
// per buffer (and concatenate) before de-interleaving -- otherwise the padding scrambles the audio.
|
// per buffer (and concatenate) before de-interleaving -- otherwise the padding scrambles the audio.
|
||||||
// This carries that self-describing capture: `bytes` holds counts[i]*stride bytes per chunk.
|
// This carries that self-describing capture: `bytes` holds counts[i]*stride bytes per chunk.
|
||||||
struct ChunkedCapture
|
struct ChunkedCapture {
|
||||||
{
|
|
||||||
unsigned stride = 0; // bytes per frame as pushed (the guessed/device block_align)
|
unsigned stride = 0; // bytes per frame as pushed (the guessed/device block_align)
|
||||||
std::vector<std::uint32_t> counts; // real frame count of each chunk
|
std::vector<std::uint32_t> counts; // real frame count of each chunk
|
||||||
std::vector<std::uint8_t> bytes; // concatenated, counts[i]*stride bytes per chunk
|
std::vector<std::uint8_t> bytes; // concatenated, counts[i]*stride bytes per chunk
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace correlate_detail
|
namespace correlate_detail {
|
||||||
{
|
|
||||||
// De-interleave raw bytes under (channels/bits/tag) and average to mono float.
|
// De-interleave raw bytes under (channels/bits/tag) and average to mono float.
|
||||||
inline void decode_layout(const std::uint8_t* bytes, std::size_t n, const LayoutCandidate& fmt,
|
inline void decode_layout(const std::uint8_t* bytes, std::size_t n, const LayoutCandidate& fmt,
|
||||||
std::vector<float>& mono)
|
std::vector<float>& mono)
|
||||||
@@ -318,33 +286,25 @@ inline void decode_layout(const std::uint8_t* bytes, std::size_t n, const Layout
|
|||||||
mono.clear();
|
mono.clear();
|
||||||
const unsigned ch = fmt.channels == 0 ? 1 : fmt.channels;
|
const unsigned ch = fmt.channels == 0 ? 1 : fmt.channels;
|
||||||
const unsigned bps = fmt.bits / 8;
|
const unsigned bps = fmt.bits / 8;
|
||||||
if (bps == 0)
|
if (bps == 0) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const std::size_t frame = static_cast<std::size_t>(ch) * bps;
|
const std::size_t frame = static_cast<std::size_t>(ch) * bps;
|
||||||
const std::size_t frames = n / frame;
|
const std::size_t frames = n / frame;
|
||||||
mono.resize(frames);
|
mono.resize(frames);
|
||||||
const bool is_float = fmt.tag == kWaveFormatFloat;
|
const bool is_float = fmt.tag == kWaveFormatFloat;
|
||||||
for (std::size_t i = 0; i < frames; ++i)
|
for (std::size_t i = 0; i < frames; ++i) {
|
||||||
{
|
|
||||||
double sum = 0.0;
|
double sum = 0.0;
|
||||||
for (unsigned c = 0; c < ch; ++c)
|
for (unsigned c = 0; c < ch; ++c) {
|
||||||
{
|
|
||||||
const std::uint8_t* p = bytes + i * frame + static_cast<std::size_t>(c) * bps;
|
const std::uint8_t* p = bytes + i * frame + static_cast<std::size_t>(c) * bps;
|
||||||
float s = 0.0f;
|
float s = 0.0f;
|
||||||
if (is_float && fmt.bits == 32)
|
if (is_float && fmt.bits == 32) {
|
||||||
{
|
|
||||||
std::memcpy(&s, p, 4);
|
std::memcpy(&s, p, 4);
|
||||||
}
|
} else if (fmt.bits == 16) {
|
||||||
else if (fmt.bits == 16)
|
|
||||||
{
|
|
||||||
std::int16_t v;
|
std::int16_t v;
|
||||||
std::memcpy(&v, p, 2);
|
std::memcpy(&v, p, 2);
|
||||||
s = v / 32768.0f;
|
s = v / 32768.0f;
|
||||||
}
|
} else if (fmt.bits == 32) {
|
||||||
else if (fmt.bits == 32)
|
|
||||||
{
|
|
||||||
std::int32_t v;
|
std::int32_t v;
|
||||||
std::memcpy(&v, p, 4);
|
std::memcpy(&v, p, 4);
|
||||||
s = static_cast<float>(v / 2147483648.0);
|
s = static_cast<float>(v / 2147483648.0);
|
||||||
@@ -372,58 +332,50 @@ inline FormatCorrelation correlate_format(const ChunkedCapture& hook, const std:
|
|||||||
double min_margin = 0.04)
|
double min_margin = 0.04)
|
||||||
{
|
{
|
||||||
FormatCorrelation result;
|
FormatCorrelation result;
|
||||||
if (hook.stride == 0 || hook.counts.empty() || loop_mono.empty() || device_rate == 0)
|
if (hook.stride == 0 || hook.counts.empty() || loop_mono.empty() || device_rate == 0) {
|
||||||
{
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
double best = -1.0, second = -1.0;
|
double best = -1.0, second = -1.0;
|
||||||
std::vector<std::uint8_t> clean;
|
std::vector<std::uint8_t> clean;
|
||||||
std::vector<float> hook_mono;
|
std::vector<float> hook_mono;
|
||||||
for (const LayoutCandidate& layout : layouts)
|
for (const LayoutCandidate& layout : layouts) {
|
||||||
{
|
|
||||||
const unsigned real_block = layout.channels * (layout.bits / 8);
|
const unsigned real_block = layout.channels * (layout.bits / 8);
|
||||||
if (real_block == 0 || real_block > hook.stride)
|
if (real_block == 0 || real_block > hook.stride) {
|
||||||
{
|
|
||||||
continue; // can't extract a frame larger than what was pushed (the guess is the max)
|
continue; // can't extract a frame larger than what was pushed (the guess is the max)
|
||||||
}
|
}
|
||||||
// Pull the real count*real_block bytes out of each padded chunk and concatenate -> contiguous
|
// Pull the real count*real_block bytes out of each padded chunk and concatenate -> contiguous
|
||||||
// audio for this candidate layout (the padding, which is stale staging bytes, is dropped).
|
// audio for this candidate layout (the padding, which is stale staging bytes, is dropped).
|
||||||
clean.clear();
|
clean.clear();
|
||||||
std::size_t off = 0;
|
std::size_t off = 0;
|
||||||
for (std::uint32_t count : hook.counts)
|
for (std::uint32_t count : hook.counts) {
|
||||||
{
|
|
||||||
const std::size_t chunk_bytes = static_cast<std::size_t>(count) * hook.stride;
|
const std::size_t chunk_bytes = static_cast<std::size_t>(count) * hook.stride;
|
||||||
const std::size_t take = static_cast<std::size_t>(count) * real_block;
|
const std::size_t take = static_cast<std::size_t>(count) * real_block;
|
||||||
if (off + chunk_bytes <= hook.bytes.size())
|
if (off + chunk_bytes <= hook.bytes.size()) {
|
||||||
{
|
|
||||||
clean.insert(clean.end(), hook.bytes.begin() + off, hook.bytes.begin() + off + take);
|
clean.insert(clean.end(), hook.bytes.begin() + off, hook.bytes.begin() + off + take);
|
||||||
}
|
}
|
||||||
off += chunk_bytes;
|
off += chunk_bytes;
|
||||||
}
|
}
|
||||||
correlate_detail::decode_layout(clean.data(), clean.size(), layout, hook_mono);
|
correlate_detail::decode_layout(clean.data(), clean.size(), layout, hook_mono);
|
||||||
if (hook_mono.size() < device_rate / 5)
|
if (hook_mono.size() < device_rate / 5) {
|
||||||
{
|
|
||||||
continue; // this layout yields too little audio to judge
|
continue; // this layout yields too little audio to judge
|
||||||
}
|
}
|
||||||
const RateCorrelation rc = correlate_rate(hook_mono, loop_mono, device_rate, rates, /*min_score=*/0.0,
|
const RateCorrelation rc = correlate_rate(hook_mono, loop_mono, device_rate, rates, /*min_score=*/0.0,
|
||||||
/*separation=*/1.0);
|
/*separation=*/1.0);
|
||||||
if (rc.score > best)
|
if (rc.score > best) {
|
||||||
{
|
|
||||||
second = best;
|
second = best;
|
||||||
best = rc.score;
|
best = rc.score;
|
||||||
result.rate = rc.rate;
|
result.rate = rc.rate;
|
||||||
result.channels = layout.channels;
|
result.channels = layout.channels;
|
||||||
result.bits = layout.bits;
|
result.bits = layout.bits;
|
||||||
result.tag = layout.tag;
|
result.tag = layout.tag;
|
||||||
}
|
} else if (rc.score > second) {
|
||||||
else if (rc.score > second)
|
|
||||||
{
|
|
||||||
second = rc.score;
|
second = rc.score;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.score = best < 0.0 ? 0.0 : best;
|
result.score = best < 0.0 ? 0.0 : best;
|
||||||
result.runner_up = second < 0.0 ? 0.0 : second;
|
result.runner_up = second < 0.0 ? 0.0 : second;
|
||||||
result.ok = result.score >= min_score && (result.runner_up <= 1e-6 || result.score - result.runner_up >= min_margin);
|
result.ok =
|
||||||
|
result.score >= min_score && (result.runner_up <= 1e-6 || result.score - result.runner_up >= min_margin);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,7 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// 'AURG' little-endian; sanity-checks the mapping before either side trusts it.
|
// 'AURG' little-endian; sanity-checks the mapping before either side trusts it.
|
||||||
inline constexpr std::uint32_t kAudioRingMagic = 0x47525541u;
|
inline constexpr std::uint32_t kAudioRingMagic = 0x47525541u;
|
||||||
@@ -30,8 +29,7 @@ inline constexpr std::uint32_t kAudioRingVersion = 2;
|
|||||||
// fields in the header. The host writes the fields then bumps op_seq; the hook applies
|
// fields in the header. The host writes the fields then bumps op_seq; the hook applies
|
||||||
// the command once per new op_seq. Lets the Audio panel re-measure a stream's rate or
|
// the command once per new op_seq. Lets the Audio panel re-measure a stream's rate or
|
||||||
// override its format when detection is wrong/unrecoverable.
|
// override its format when detection is wrong/unrecoverable.
|
||||||
enum AudioRingOp : std::uint32_t
|
enum AudioRingOp : std::uint32_t {
|
||||||
{
|
|
||||||
AudioRingOp_None = 0,
|
AudioRingOp_None = 0,
|
||||||
AudioRingOp_Remeasure = 1, // re-run the sample-rate measurement for this stream
|
AudioRingOp_Remeasure = 1, // re-run the sample-rate measurement for this stream
|
||||||
AudioRingOp_Override = 2, // adopt the op_rate/channels/bits/format_tag verbatim
|
AudioRingOp_Override = 2, // adopt the op_rate/channels/bits/format_tag verbatim
|
||||||
@@ -48,8 +46,7 @@ inline constexpr std::uint32_t kAudioRingCapacity = 1u << 20;
|
|||||||
// the format fields are written once by the producer *before* it publishes
|
// the format fields are written once by the producer *before* it publishes
|
||||||
// format_valid (release), and read by the consumer *after* it observes
|
// format_valid (release), and read by the consumer *after* it observes
|
||||||
// format_valid (acquire), so they need no atomicity of their own.
|
// format_valid (acquire), so they need no atomicity of their own.
|
||||||
struct AudioRingHeader
|
struct AudioRingHeader {
|
||||||
{
|
|
||||||
std::uint32_t magic;
|
std::uint32_t magic;
|
||||||
std::uint32_t version;
|
std::uint32_t version;
|
||||||
|
|
||||||
@@ -176,8 +173,7 @@ inline bool audio_ring_push(AudioRingHeader& h, const void* src, std::uint32_t b
|
|||||||
const std::uint64_t w = h.write_pos.load(std::memory_order_relaxed);
|
const std::uint64_t w = h.write_pos.load(std::memory_order_relaxed);
|
||||||
const std::uint64_t r = h.read_pos.load(std::memory_order_acquire);
|
const std::uint64_t r = h.read_pos.load(std::memory_order_acquire);
|
||||||
const std::uint32_t used = static_cast<std::uint32_t>(w - r);
|
const std::uint32_t used = static_cast<std::uint32_t>(w - r);
|
||||||
if (bytes > h.capacity - used)
|
if (bytes > h.capacity - used) {
|
||||||
{
|
|
||||||
h.overruns.fetch_add(1, std::memory_order_relaxed);
|
h.overruns.fetch_add(1, std::memory_order_relaxed);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -185,8 +181,7 @@ inline bool audio_ring_push(AudioRingHeader& h, const void* src, std::uint32_t b
|
|||||||
const std::uint32_t off = static_cast<std::uint32_t>(w % h.capacity);
|
const std::uint32_t off = static_cast<std::uint32_t>(w % h.capacity);
|
||||||
const std::uint32_t first = std::min(bytes, h.capacity - off);
|
const std::uint32_t first = std::min(bytes, h.capacity - off);
|
||||||
std::memcpy(data + off, src, first);
|
std::memcpy(data + off, src, first);
|
||||||
if (bytes > first)
|
if (bytes > first) {
|
||||||
{
|
|
||||||
std::memcpy(data, static_cast<const std::uint8_t*>(src) + first, bytes - first);
|
std::memcpy(data, static_cast<const std::uint8_t*>(src) + first, bytes - first);
|
||||||
}
|
}
|
||||||
h.write_pos.store(w + bytes, std::memory_order_release);
|
h.write_pos.store(w + bytes, std::memory_order_release);
|
||||||
@@ -222,8 +217,7 @@ inline std::uint32_t audio_ring_pop(AudioRingHeader& h, void* dst, std::uint32_t
|
|||||||
const std::uint32_t off = static_cast<std::uint32_t>(r % h.capacity);
|
const std::uint32_t off = static_cast<std::uint32_t>(r % h.capacity);
|
||||||
const std::uint32_t first = std::min(bytes, h.capacity - off);
|
const std::uint32_t first = std::min(bytes, h.capacity - off);
|
||||||
std::memcpy(dst, data + off, first);
|
std::memcpy(dst, data + off, first);
|
||||||
if (bytes > first)
|
if (bytes > first) {
|
||||||
{
|
|
||||||
std::memcpy(static_cast<std::uint8_t*>(dst) + first, data, bytes - first);
|
std::memcpy(static_cast<std::uint8_t*>(dst) + first, data, bytes - first);
|
||||||
}
|
}
|
||||||
h.read_pos.store(r + bytes, std::memory_order_release);
|
h.read_pos.store(r + bytes, std::memory_order_release);
|
||||||
@@ -234,8 +228,7 @@ inline std::uint32_t audio_ring_pop(AudioRingHeader& h, void* dst, std::uint32_t
|
|||||||
// bumps op_seq (release) so the hook applies it exactly once. For a re-measure the
|
// bumps op_seq (release) so the hook applies it exactly once. For a re-measure the
|
||||||
// rate/channels/bits are ignored.
|
// rate/channels/bits are ignored.
|
||||||
inline void audio_ring_post_op(AudioRingHeader& h, std::uint32_t kind, std::uint32_t rate = 0,
|
inline void audio_ring_post_op(AudioRingHeader& h, std::uint32_t kind, std::uint32_t rate = 0,
|
||||||
std::uint32_t channels = 0, std::uint32_t bits = 0,
|
std::uint32_t channels = 0, std::uint32_t bits = 0, std::uint32_t format_tag = 0)
|
||||||
std::uint32_t format_tag = 0)
|
|
||||||
{
|
{
|
||||||
h.op_kind = kind;
|
h.op_kind = kind;
|
||||||
h.op_rate = rate;
|
h.op_rate = rate;
|
||||||
@@ -246,8 +239,7 @@ inline void audio_ring_post_op(AudioRingHeader& h, std::uint32_t kind, std::uint
|
|||||||
}
|
}
|
||||||
|
|
||||||
// One operator command read back by the hook.
|
// One operator command read back by the hook.
|
||||||
struct AudioRingOpCmd
|
struct AudioRingOpCmd {
|
||||||
{
|
|
||||||
std::uint32_t kind = AudioRingOp_None;
|
std::uint32_t kind = AudioRingOp_None;
|
||||||
std::uint32_t rate = 0;
|
std::uint32_t rate = 0;
|
||||||
std::uint32_t channels = 0;
|
std::uint32_t channels = 0;
|
||||||
@@ -261,8 +253,7 @@ struct AudioRingOpCmd
|
|||||||
inline std::uint32_t audio_ring_poll_op(AudioRingHeader& h, std::uint32_t& last_seq, AudioRingOpCmd& out)
|
inline std::uint32_t audio_ring_poll_op(AudioRingHeader& h, std::uint32_t& last_seq, AudioRingOpCmd& out)
|
||||||
{
|
{
|
||||||
const std::uint32_t seq = h.op_seq.load(std::memory_order_acquire);
|
const std::uint32_t seq = h.op_seq.load(std::memory_order_acquire);
|
||||||
if (seq == last_seq)
|
if (seq == last_seq) {
|
||||||
{
|
|
||||||
return AudioRingOp_None;
|
return AudioRingOp_None;
|
||||||
}
|
}
|
||||||
last_seq = seq;
|
last_seq = seq;
|
||||||
@@ -280,8 +271,7 @@ inline std::uint32_t audio_ring_poll_op(AudioRingHeader& h, std::uint32_t& last_
|
|||||||
inline std::wstring audio_ring_name(unsigned long target_pid, unsigned index = 0)
|
inline std::wstring audio_ring_name(unsigned long target_pid, unsigned index = 0)
|
||||||
{
|
{
|
||||||
std::wstring name = std::wstring(kAudioRingPrefix) + std::to_wstring(target_pid);
|
std::wstring name = std::wstring(kAudioRingPrefix) + std::to_wstring(target_pid);
|
||||||
if (index != 0)
|
if (index != 0) {
|
||||||
{
|
|
||||||
name += L"_" + std::to_wstring(index);
|
name += L"_" + std::to_wstring(index);
|
||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
|
|
||||||
#include <windows.h> // USER_DEFAULT_SCREEN_DPI (== 96, the 100%-scale baseline)
|
#include <windows.h> // USER_DEFAULT_SCREEN_DPI (== 96, the 100%-scale baseline)
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// ImGui's built-in default font (ProggyClean) rasterizes at this pixel size at 100% scale. Named once
|
// ImGui's built-in default font (ProggyClean) rasterizes at this pixel size at 100% scale. Named once
|
||||||
// here so the DPI math scales from a single owned constant instead of a bare 13 sprinkled around.
|
// here so the DPI math scales from a single owned constant instead of a bare 13 sprinkled around.
|
||||||
@@ -26,8 +25,7 @@ inline constexpr float kMaxUiScale = 8.0f;
|
|||||||
// callers never derive a zero-size font.
|
// callers never derive a zero-size font.
|
||||||
inline float dpi_scale_from(unsigned dpi)
|
inline float dpi_scale_from(unsigned dpi)
|
||||||
{
|
{
|
||||||
if (dpi == 0)
|
if (dpi == 0) {
|
||||||
{
|
|
||||||
dpi = USER_DEFAULT_SCREEN_DPI;
|
dpi = USER_DEFAULT_SCREEN_DPI;
|
||||||
}
|
}
|
||||||
const float scale = static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
|
const float scale = static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
|
||||||
|
|||||||
@@ -15,8 +15,7 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// 'CLOG' little-endian.
|
// 'CLOG' little-endian.
|
||||||
inline constexpr std::uint32_t kLogRingMagic = 0x474F4C43u;
|
inline constexpr std::uint32_t kLogRingMagic = 0x474F4C43u;
|
||||||
@@ -30,15 +29,13 @@ inline constexpr std::uint32_t kLogCapacity = 1024; // ring records
|
|||||||
|
|
||||||
// Severity of a log line; drives the host Log window's colour. Stored in
|
// Severity of a log line; drives the host Log window's colour. Stored in
|
||||||
// LogRecord::level. Info is 0 so existing/zero-filled records read as Info.
|
// LogRecord::level. Info is 0 so existing/zero-filled records read as Info.
|
||||||
enum LogLevel : std::uint32_t
|
enum LogLevel : std::uint32_t {
|
||||||
{
|
|
||||||
LogLevel_Info = 0,
|
LogLevel_Info = 0,
|
||||||
LogLevel_Warn = 1,
|
LogLevel_Warn = 1,
|
||||||
LogLevel_Error = 2,
|
LogLevel_Error = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LogRecord
|
struct LogRecord {
|
||||||
{
|
|
||||||
std::atomic<std::uint64_t> seq; // 0 = empty; else (global index + 1) once written
|
std::atomic<std::uint64_t> seq; // 0 = empty; else (global index + 1) once written
|
||||||
std::uint32_t pid;
|
std::uint32_t pid;
|
||||||
std::uint32_t level; // LogLevel
|
std::uint32_t level; // LogLevel
|
||||||
@@ -46,8 +43,7 @@ struct LogRecord
|
|||||||
char text[kLogMsgLen];
|
char text[kLogMsgLen];
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LogRing
|
struct LogRing {
|
||||||
{
|
|
||||||
std::uint32_t magic;
|
std::uint32_t magic;
|
||||||
std::uint32_t version;
|
std::uint32_t version;
|
||||||
std::uint32_t capacity; // number of records
|
std::uint32_t capacity; // number of records
|
||||||
@@ -83,13 +79,11 @@ inline void log_ring_init(LogRing& r, std::uint32_t capacity)
|
|||||||
|
|
||||||
inline bool log_ring_valid(const LogRing& r)
|
inline bool log_ring_valid(const LogRing& r)
|
||||||
{
|
{
|
||||||
return r.magic == kLogRingMagic && r.version == kLogRingVersion && r.capacity != 0 &&
|
return r.magic == kLogRingMagic && r.version == kLogRingVersion && r.capacity != 0 && r.msg_len == kLogMsgLen;
|
||||||
r.msg_len == kLogMsgLen;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Producer (hook): append a line at severity `level` (LogLevel). Multi-producer safe.
|
// Producer (hook): append a line at severity `level` (LogLevel). Multi-producer safe.
|
||||||
inline void log_ring_push(LogRing& r, std::uint32_t pid, std::uint32_t level, std::uint64_t millis,
|
inline void log_ring_push(LogRing& r, std::uint32_t pid, std::uint32_t level, std::uint64_t millis, const char* text)
|
||||||
const char* text)
|
|
||||||
{
|
{
|
||||||
const std::uint64_t idx = r.write_index.fetch_add(1, std::memory_order_acq_rel);
|
const std::uint64_t idx = r.write_index.fetch_add(1, std::memory_order_acq_rel);
|
||||||
LogRecord& rec = log_ring_records(&r)[idx % r.capacity];
|
LogRecord& rec = log_ring_records(&r)[idx % r.capacity];
|
||||||
@@ -115,23 +109,19 @@ template <typename F>
|
|||||||
inline void log_ring_drain(LogRing& r, std::uint64_t& cursor, F&& emit)
|
inline void log_ring_drain(LogRing& r, std::uint64_t& cursor, F&& emit)
|
||||||
{
|
{
|
||||||
const std::uint64_t w = r.write_index.load(std::memory_order_acquire);
|
const std::uint64_t w = r.write_index.load(std::memory_order_acquire);
|
||||||
if (w <= cursor)
|
if (w <= cursor) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const std::uint64_t lo = (w > r.capacity) ? (w - r.capacity) : 0;
|
const std::uint64_t lo = (w > r.capacity) ? (w - r.capacity) : 0;
|
||||||
std::uint64_t i = cursor < lo ? lo : cursor; // skip records already overwritten
|
std::uint64_t i = cursor < lo ? lo : cursor; // skip records already overwritten
|
||||||
LogRecord* recs = log_ring_records(&r);
|
LogRecord* recs = log_ring_records(&r);
|
||||||
for (; i < w; ++i)
|
for (; i < w; ++i) {
|
||||||
{
|
|
||||||
LogRecord& rec = recs[i % r.capacity];
|
LogRecord& rec = recs[i % r.capacity];
|
||||||
const std::uint64_t s1 = rec.seq.load(std::memory_order_acquire);
|
const std::uint64_t s1 = rec.seq.load(std::memory_order_acquire);
|
||||||
if (s1 <= i)
|
if (s1 <= i) {
|
||||||
{
|
|
||||||
break; // generation i not written yet (in-flight, or being overwritten); retry next call
|
break; // generation i not written yet (in-flight, or being overwritten); retry next call
|
||||||
}
|
}
|
||||||
if (s1 != i + 1)
|
if (s1 != i + 1) {
|
||||||
{
|
|
||||||
continue; // s1 > i+1: overwritten by a later generation before we got here; lost, skip
|
continue; // s1 > i+1: overwritten by a later generation before we got here; lost, skip
|
||||||
}
|
}
|
||||||
// Seqlock read: copy the record out, then re-check seq. A producer overwriting this slot stores
|
// Seqlock read: copy the record out, then re-check seq. A producer overwriting this slot stores
|
||||||
@@ -142,8 +132,7 @@ inline void log_ring_drain(LogRing& r, std::uint64_t& cursor, F&& emit)
|
|||||||
snap.millis = rec.millis;
|
snap.millis = rec.millis;
|
||||||
std::memcpy(snap.text, rec.text, kLogMsgLen);
|
std::memcpy(snap.text, rec.text, kLogMsgLen);
|
||||||
std::atomic_thread_fence(std::memory_order_acquire);
|
std::atomic_thread_fence(std::memory_order_acquire);
|
||||||
if (rec.seq.load(std::memory_order_relaxed) == i + 1)
|
if (rec.seq.load(std::memory_order_relaxed) == i + 1) {
|
||||||
{
|
|
||||||
emit(snap); // consistent snapshot
|
emit(snap); // consistent snapshot
|
||||||
}
|
}
|
||||||
// else: overwritten while we copied -> skip (lost)
|
// else: overwritten while we copied -> skip (lost)
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// Bump whenever the layout of SharedBlock or CoopPadState changes. The hook
|
// Bump whenever the layout of SharedBlock or CoopPadState changes. The hook
|
||||||
// refuses to attach to a host with a mismatched version.
|
// refuses to attach to a host with a mismatched version.
|
||||||
@@ -27,8 +26,7 @@ inline constexpr wchar_t kSharedMemoryPrefix[] = L"Local\\coop_ipc_";
|
|||||||
// One controller's state, laid out to map 1:1 onto XINPUT_GAMEPAD plus the
|
// One controller's state, laid out to map 1:1 onto XINPUT_GAMEPAD plus the
|
||||||
// metadata the hook needs. Field names/types match XINPUT_GAMEPAD so the hook
|
// metadata the hook needs. Field names/types match XINPUT_GAMEPAD so the hook
|
||||||
// can memcpy the trailing region straight into an XINPUT_STATE.
|
// can memcpy the trailing region straight into an XINPUT_STATE.
|
||||||
struct CoopPadState
|
struct CoopPadState {
|
||||||
{
|
|
||||||
std::uint8_t connected; // 1 if a guest/host pad is mapped to this slot
|
std::uint8_t connected; // 1 if a guest/host pad is mapped to this slot
|
||||||
std::uint8_t reserved[3];
|
std::uint8_t reserved[3];
|
||||||
std::uint32_t packet; // bumps on change -> XINPUT_STATE::dwPacketNumber
|
std::uint32_t packet; // bumps on change -> XINPUT_STATE::dwPacketNumber
|
||||||
@@ -56,8 +54,7 @@ inline constexpr std::uint32_t kMaxAudioStreams = 4;
|
|||||||
// we injected (the common case) was never seen at Initialize, so its format starts as a
|
// we injected (the common case) was never seen at Initialize, so its format starts as a
|
||||||
// guess (the device mix format) and its true sample rate is measured from the render
|
// guess (the device mix format) and its true sample rate is measured from the render
|
||||||
// cadence; a stream we watched get created carries its exact Initialize format.
|
// cadence; a stream we watched get created carries its exact Initialize format.
|
||||||
enum AudioFormatState : std::uint32_t
|
enum AudioFormatState : std::uint32_t {
|
||||||
{
|
|
||||||
AudioFormat_Unknown = 0, // no format determined yet
|
AudioFormat_Unknown = 0, // no format determined yet
|
||||||
AudioFormat_Exact = 1, // taken from the game's own IAudioClient::Initialize
|
AudioFormat_Exact = 1, // taken from the game's own IAudioClient::Initialize
|
||||||
AudioFormat_Measuring = 2, // guessed (device mix format); true sample rate being measured
|
AudioFormat_Measuring = 2, // guessed (device mix format); true sample rate being measured
|
||||||
@@ -66,8 +63,7 @@ enum AudioFormatState : std::uint32_t
|
|||||||
AudioFormat_Override = 5, // operator set this format manually (see the per-stream op channel)
|
AudioFormat_Override = 5, // operator set this format manually (see the per-stream op channel)
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AudioStreamInfo
|
struct AudioStreamInfo {
|
||||||
{
|
|
||||||
std::uint32_t is_primary; // 1 = the stream the hook captures/silences
|
std::uint32_t is_primary; // 1 = the stream the hook captures/silences
|
||||||
std::uint32_t sample_rate;
|
std::uint32_t sample_rate;
|
||||||
std::uint16_t channels;
|
std::uint16_t channels;
|
||||||
@@ -78,8 +74,7 @@ struct AudioStreamInfo
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Orthogonal hook subsystems the host can install/remove independently.
|
// Orthogonal hook subsystems the host can install/remove independently.
|
||||||
enum HookSubsystem : std::uint32_t
|
enum HookSubsystem : std::uint32_t {
|
||||||
{
|
|
||||||
HookSubsys_Input = 0, // XInput hooks (forward the guest pad)
|
HookSubsys_Input = 0, // XInput hooks (forward the guest pad)
|
||||||
HookSubsys_Focus = 1, // focus spoof (keep the game running unfocused)
|
HookSubsys_Focus = 1, // focus spoof (keep the game running unfocused)
|
||||||
HookSubsys_Audio = 2, // WASAPI render-hook (audio mirror without echo)
|
HookSubsys_Audio = 2, // WASAPI render-hook (audio mirror without echo)
|
||||||
@@ -93,8 +88,7 @@ inline constexpr std::uint32_t kMaxHookEntries = 24;
|
|||||||
|
|
||||||
// One installed hook, for the Injection panel's hook list. POD diagnostics, like
|
// One installed hook, for the Injection panel's hook list. POD diagnostics, like
|
||||||
// AudioStreamInfo: the hook is the sole writer; benign cross-process races are ok.
|
// AudioStreamInfo: the hook is the sole writer; benign cross-process races are ok.
|
||||||
struct HookEntry
|
struct HookEntry {
|
||||||
{
|
|
||||||
char name[40]; // e.g. "XInputGetState"
|
char name[40]; // e.g. "XInputGetState"
|
||||||
std::uint32_t subsystem; // HookSubsystem
|
std::uint32_t subsystem; // HookSubsystem
|
||||||
std::uint32_t installed; // 1 if currently hooked
|
std::uint32_t installed; // 1 if currently hooked
|
||||||
@@ -102,8 +96,7 @@ struct HookEntry
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Indices into HookStatus::focus_query_calls.
|
// Indices into HookStatus::focus_query_calls.
|
||||||
enum FocusApi : std::uint32_t
|
enum FocusApi : std::uint32_t {
|
||||||
{
|
|
||||||
FocusApi_Foreground = 0, // GetForegroundWindow
|
FocusApi_Foreground = 0, // GetForegroundWindow
|
||||||
FocusApi_Active = 1, // GetActiveWindow
|
FocusApi_Active = 1, // GetActiveWindow
|
||||||
FocusApi_Focus = 2, // GetFocus
|
FocusApi_Focus = 2, // GetFocus
|
||||||
@@ -115,8 +108,7 @@ enum FocusApi : std::uint32_t
|
|||||||
// polling, does it use the focus APIs, and does it read input through a
|
// polling, does it use the focus APIs, and does it read input through a
|
||||||
// focus-gated path (Raw Input / DirectInput)? Diagnostics only, so the non-atomic
|
// focus-gated path (Raw Input / DirectInput)? Diagnostics only, so the non-atomic
|
||||||
// fields tolerate benign cross-process races.
|
// fields tolerate benign cross-process races.
|
||||||
struct HookStatus
|
struct HookStatus {
|
||||||
{
|
|
||||||
std::atomic<std::uint32_t> heartbeat; // DLL bumps ~4x/sec while alive
|
std::atomic<std::uint32_t> heartbeat; // DLL bumps ~4x/sec while alive
|
||||||
std::atomic<std::uint64_t> get_state_calls[kMaxPads]; // XInputGetState/Ex per slot
|
std::atomic<std::uint64_t> get_state_calls[kMaxPads]; // XInputGetState/Ex per slot
|
||||||
std::atomic<std::uint64_t> get_caps_calls[kMaxPads]; // XInputGetCapabilities per slot
|
std::atomic<std::uint64_t> get_caps_calls[kMaxPads]; // XInputGetCapabilities per slot
|
||||||
@@ -163,8 +155,7 @@ struct HookStatus
|
|||||||
// Host -> hook control channel. The host requests which hook subsystems should be
|
// Host -> hook control channel. The host requests which hook subsystems should be
|
||||||
// installed; the hook reconciles each tick. 0 = install (the zero-filled default,
|
// installed; the hook reconciles each tick. 0 = install (the zero-filled default,
|
||||||
// so a fresh mapping installs everything as before), 1 = remove.
|
// so a fresh mapping installs everything as before), 1 = remove.
|
||||||
struct HookControl
|
struct HookControl {
|
||||||
{
|
|
||||||
std::atomic<std::uint32_t> subsystem_disabled[HookSubsys_Count];
|
std::atomic<std::uint32_t> subsystem_disabled[HookSubsys_Count];
|
||||||
|
|
||||||
// Cursor handling for cursor-clipping games (part of the Focus subsystem).
|
// Cursor handling for cursor-clipping games (part of the Focus subsystem).
|
||||||
@@ -181,8 +172,7 @@ struct HookControl
|
|||||||
// is the sole writer. `generation` bumps on every published frame (0 = nothing
|
// is the sole writer. `generation` bumps on every published frame (0 = nothing
|
||||||
// shared yet); width/height/format describe the currently shared texture, so the
|
// shared yet); width/height/format describe the currently shared texture, so the
|
||||||
// host reopens it whenever they change. The keyed mutex uses key 0 on both sides.
|
// host reopens it whenever they change. The keyed mutex uses key 0 on both sides.
|
||||||
struct VideoShare
|
struct VideoShare {
|
||||||
{
|
|
||||||
std::atomic<std::uint32_t> generation; // bumps per published frame; 0 = none yet
|
std::atomic<std::uint32_t> generation; // bumps per published frame; 0 = none yet
|
||||||
std::uint32_t width; // shared texture dimensions / DXGI format
|
std::uint32_t width; // shared texture dimensions / DXGI format
|
||||||
std::uint32_t height;
|
std::uint32_t height;
|
||||||
@@ -203,8 +193,7 @@ struct VideoShare
|
|||||||
// the matching window messages to the game, and maintains a synthesized state the
|
// the matching window messages to the game, and maintains a synthesized state the
|
||||||
// GetAsyncKeyState/GetKeyboardState/GetCursorPos hooks report to polling games.
|
// GetAsyncKeyState/GetKeyboardState/GetCursorPos hooks report to polling games.
|
||||||
|
|
||||||
enum MkbEventType : std::uint32_t
|
enum MkbEventType : std::uint32_t {
|
||||||
{
|
|
||||||
Mkb_KeyDown = 0, // code = Win32 virtual-key
|
Mkb_KeyDown = 0, // code = Win32 virtual-key
|
||||||
Mkb_KeyUp = 1, // code = Win32 virtual-key
|
Mkb_KeyUp = 1, // code = Win32 virtual-key
|
||||||
Mkb_Char = 2, // code = UTF-16 code unit (WM_CHAR)
|
Mkb_Char = 2, // code = UTF-16 code unit (WM_CHAR)
|
||||||
@@ -213,8 +202,7 @@ enum MkbEventType : std::uint32_t
|
|||||||
Mkb_Wheel = 5, // code = signed wheel delta (WHEEL_DELTA units); x,y = game client px
|
Mkb_Wheel = 5, // code = signed wheel delta (WHEEL_DELTA units); x,y = game client px
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MkbEvent
|
struct MkbEvent {
|
||||||
{
|
|
||||||
std::uint32_t type; // MkbEventType
|
std::uint32_t type; // MkbEventType
|
||||||
std::uint32_t code; // see per-type meaning above
|
std::uint32_t code; // see per-type meaning above
|
||||||
std::int32_t x; // game-client x (mouse events)
|
std::int32_t x; // game-client x (mouse events)
|
||||||
@@ -227,8 +215,7 @@ static_assert(sizeof(MkbEvent) == 16, "MkbEvent must stay byte-identical across
|
|||||||
inline constexpr std::uint32_t kMkbQueueSize = 128;
|
inline constexpr std::uint32_t kMkbQueueSize = 128;
|
||||||
|
|
||||||
// Lock-free SPSC ring: host produces, hook consumes. Free-running 32-bit indices.
|
// Lock-free SPSC ring: host produces, hook consumes. Free-running 32-bit indices.
|
||||||
struct MkbRing
|
struct MkbRing {
|
||||||
{
|
|
||||||
std::atomic<std::uint32_t> head; // producer (host) write position
|
std::atomic<std::uint32_t> head; // producer (host) write position
|
||||||
std::atomic<std::uint32_t> tail; // consumer (hook) read position
|
std::atomic<std::uint32_t> tail; // consumer (hook) read position
|
||||||
MkbEvent events[kMkbQueueSize];
|
MkbEvent events[kMkbQueueSize];
|
||||||
@@ -244,8 +231,7 @@ inline constexpr std::uint64_t kVideoMutexKey = 0;
|
|||||||
// Top-level shared block. The host is the sole writer of pad state; the hook is
|
// Top-level shared block. The host is the sole writer of pad state; the hook is
|
||||||
// the sole reader. A seqlock (even = stable, odd = write in progress) lets the
|
// the sole reader. A seqlock (even = stable, odd = write in progress) lets the
|
||||||
// reader grab a torn-free snapshot without a kernel lock on the hot path.
|
// reader grab a torn-free snapshot without a kernel lock on the hot path.
|
||||||
struct SharedBlock
|
struct SharedBlock {
|
||||||
{
|
|
||||||
std::uint32_t magic;
|
std::uint32_t magic;
|
||||||
std::uint32_t version;
|
std::uint32_t version;
|
||||||
std::uint32_t pad_count; // number of populated slots, <= kMaxPads
|
std::uint32_t pad_count; // number of populated slots, <= kMaxPads
|
||||||
@@ -297,20 +283,17 @@ static_assert(sizeof(MkbRing) == 2056, "MkbRing size changed -- wire-protocol ch
|
|||||||
// Writer side: publish a fresh set of pad states. Called from the host.
|
// Writer side: publish a fresh set of pad states. Called from the host.
|
||||||
inline void publish_pads(SharedBlock& block, const CoopPadState* pads, std::uint32_t count)
|
inline void publish_pads(SharedBlock& block, const CoopPadState* pads, std::uint32_t count)
|
||||||
{
|
{
|
||||||
if (count > kMaxPads)
|
if (count > kMaxPads) {
|
||||||
{
|
|
||||||
count = kMaxPads;
|
count = kMaxPads;
|
||||||
}
|
}
|
||||||
const std::uint32_t seq = block.sequence.load(std::memory_order_relaxed);
|
const std::uint32_t seq = block.sequence.load(std::memory_order_relaxed);
|
||||||
block.sequence.store(seq + 1, std::memory_order_release); // -> odd: write begins
|
block.sequence.store(seq + 1, std::memory_order_release); // -> odd: write begins
|
||||||
std::atomic_thread_fence(std::memory_order_release);
|
std::atomic_thread_fence(std::memory_order_release);
|
||||||
block.pad_count = count;
|
block.pad_count = count;
|
||||||
for (std::uint32_t i = 0; i < count; ++i)
|
for (std::uint32_t i = 0; i < count; ++i) {
|
||||||
{
|
|
||||||
block.pads[i] = pads[i];
|
block.pads[i] = pads[i];
|
||||||
}
|
}
|
||||||
for (std::uint32_t i = count; i < kMaxPads; ++i)
|
for (std::uint32_t i = count; i < kMaxPads; ++i) {
|
||||||
{
|
|
||||||
block.pads[i] = CoopPadState{};
|
block.pads[i] = CoopPadState{};
|
||||||
}
|
}
|
||||||
block.sequence.store(seq + 2, std::memory_order_release); // -> even: write done
|
block.sequence.store(seq + 2, std::memory_order_release); // -> even: write done
|
||||||
@@ -320,26 +303,21 @@ inline void publish_pads(SharedBlock& block, const CoopPadState* pads, std::uint
|
|||||||
// if a write is in flight; bounded so a crashed writer can't hang the game.
|
// if a write is in flight; bounded so a crashed writer can't hang the game.
|
||||||
inline bool read_pads(const SharedBlock& block, CoopPadState (&out)[kMaxPads], std::uint32_t& out_count)
|
inline bool read_pads(const SharedBlock& block, CoopPadState (&out)[kMaxPads], std::uint32_t& out_count)
|
||||||
{
|
{
|
||||||
for (int attempt = 0; attempt < 64; ++attempt)
|
for (int attempt = 0; attempt < 64; ++attempt) {
|
||||||
{
|
|
||||||
const std::uint32_t before = block.sequence.load(std::memory_order_acquire);
|
const std::uint32_t before = block.sequence.load(std::memory_order_acquire);
|
||||||
if (before & 1u)
|
if (before & 1u) {
|
||||||
{
|
|
||||||
continue; // writer mid-update, retry
|
continue; // writer mid-update, retry
|
||||||
}
|
}
|
||||||
std::uint32_t count = block.pad_count;
|
std::uint32_t count = block.pad_count;
|
||||||
if (count > kMaxPads)
|
if (count > kMaxPads) {
|
||||||
{
|
|
||||||
count = kMaxPads;
|
count = kMaxPads;
|
||||||
}
|
}
|
||||||
for (std::uint32_t i = 0; i < kMaxPads; ++i)
|
for (std::uint32_t i = 0; i < kMaxPads; ++i) {
|
||||||
{
|
|
||||||
out[i] = block.pads[i];
|
out[i] = block.pads[i];
|
||||||
}
|
}
|
||||||
std::atomic_thread_fence(std::memory_order_acquire);
|
std::atomic_thread_fence(std::memory_order_acquire);
|
||||||
const std::uint32_t after = block.sequence.load(std::memory_order_acquire);
|
const std::uint32_t after = block.sequence.load(std::memory_order_acquire);
|
||||||
if (before == after)
|
if (before == after) {
|
||||||
{
|
|
||||||
out_count = count;
|
out_count = count;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -354,8 +332,7 @@ inline bool push_mkb_event(MkbRing& ring, const MkbEvent& ev)
|
|||||||
{
|
{
|
||||||
const std::uint32_t head = ring.head.load(std::memory_order_relaxed);
|
const std::uint32_t head = ring.head.load(std::memory_order_relaxed);
|
||||||
const std::uint32_t tail = ring.tail.load(std::memory_order_acquire);
|
const std::uint32_t tail = ring.tail.load(std::memory_order_acquire);
|
||||||
if (head - tail >= kMkbQueueSize)
|
if (head - tail >= kMkbQueueSize) {
|
||||||
{
|
|
||||||
return false; // full -> drop (host should always drain faster than it fills)
|
return false; // full -> drop (host should always drain faster than it fills)
|
||||||
}
|
}
|
||||||
ring.events[head & (kMkbQueueSize - 1)] = ev;
|
ring.events[head & (kMkbQueueSize - 1)] = ev;
|
||||||
@@ -368,8 +345,7 @@ inline bool pop_mkb_event(MkbRing& ring, MkbEvent& out)
|
|||||||
{
|
{
|
||||||
const std::uint32_t tail = ring.tail.load(std::memory_order_relaxed);
|
const std::uint32_t tail = ring.tail.load(std::memory_order_relaxed);
|
||||||
const std::uint32_t head = ring.head.load(std::memory_order_acquire);
|
const std::uint32_t head = ring.head.load(std::memory_order_acquire);
|
||||||
if (tail == head)
|
if (tail == head) {
|
||||||
{
|
|
||||||
return false; // empty
|
return false; // empty
|
||||||
}
|
}
|
||||||
out = ring.events[tail & (kMkbQueueSize - 1)];
|
out = ring.events[tail & (kMkbQueueSize - 1)];
|
||||||
|
|||||||
@@ -9,26 +9,20 @@
|
|||||||
|
|
||||||
#include "coop/protocol.hpp"
|
#include "coop/protocol.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
class SharedMemory
|
class SharedMemory {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
SharedMemory() = default;
|
SharedMemory() = default;
|
||||||
|
|
||||||
SharedMemory(const SharedMemory&) = delete;
|
SharedMemory(const SharedMemory&) = delete;
|
||||||
SharedMemory& operator=(const SharedMemory&) = delete;
|
SharedMemory& operator=(const SharedMemory&) = delete;
|
||||||
|
|
||||||
SharedMemory(SharedMemory&& other) noexcept
|
SharedMemory(SharedMemory&& other) noexcept { *this = std::move(other); }
|
||||||
{
|
|
||||||
*this = std::move(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
SharedMemory& operator=(SharedMemory&& other) noexcept
|
SharedMemory& operator=(SharedMemory&& other) noexcept
|
||||||
{
|
{
|
||||||
if (this != &other)
|
if (this != &other) {
|
||||||
{
|
|
||||||
reset();
|
reset();
|
||||||
mapping_ = std::exchange(other.mapping_, nullptr);
|
mapping_ = std::exchange(other.mapping_, nullptr);
|
||||||
view_ = std::exchange(other.view_, nullptr);
|
view_ = std::exchange(other.view_, nullptr);
|
||||||
@@ -37,19 +31,15 @@ public:
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
~SharedMemory()
|
~SharedMemory() { reset(); }
|
||||||
{
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Host side: create (or open if it already exists) the named section.
|
// Host side: create (or open if it already exists) the named section.
|
||||||
bool create(const std::wstring& name, std::size_t size)
|
bool create(const std::wstring& name, std::size_t size)
|
||||||
{
|
{
|
||||||
reset();
|
reset();
|
||||||
mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0,
|
mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, static_cast<DWORD>(size),
|
||||||
static_cast<DWORD>(size), name.c_str());
|
name.c_str());
|
||||||
if (mapping_ == nullptr)
|
if (mapping_ == nullptr) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return map(size);
|
return map(size);
|
||||||
@@ -60,8 +50,7 @@ public:
|
|||||||
{
|
{
|
||||||
reset();
|
reset();
|
||||||
mapping_ = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, name.c_str());
|
mapping_ = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, name.c_str());
|
||||||
if (mapping_ == nullptr)
|
if (mapping_ == nullptr) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return map(size);
|
return map(size);
|
||||||
@@ -69,23 +58,18 @@ public:
|
|||||||
|
|
||||||
void reset()
|
void reset()
|
||||||
{
|
{
|
||||||
if (view_ != nullptr)
|
if (view_ != nullptr) {
|
||||||
{
|
|
||||||
UnmapViewOfFile(view_);
|
UnmapViewOfFile(view_);
|
||||||
view_ = nullptr;
|
view_ = nullptr;
|
||||||
}
|
}
|
||||||
if (mapping_ != nullptr)
|
if (mapping_ != nullptr) {
|
||||||
{
|
|
||||||
CloseHandle(mapping_);
|
CloseHandle(mapping_);
|
||||||
mapping_ = nullptr;
|
mapping_ = nullptr;
|
||||||
}
|
}
|
||||||
size_ = 0;
|
size_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool valid() const
|
[[nodiscard]] bool valid() const { return view_ != nullptr; }
|
||||||
{
|
|
||||||
return view_ != nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
[[nodiscard]] T* as() const
|
[[nodiscard]] T* as() const
|
||||||
@@ -93,22 +77,15 @@ public:
|
|||||||
return static_cast<T*>(view_);
|
return static_cast<T*>(view_);
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] void* data() const
|
[[nodiscard]] void* data() const { return view_; }
|
||||||
{
|
|
||||||
return view_;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] std::size_t size() const
|
[[nodiscard]] std::size_t size() const { return size_; }
|
||||||
{
|
|
||||||
return size_;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool map(std::size_t size)
|
bool map(std::size_t size)
|
||||||
{
|
{
|
||||||
view_ = MapViewOfFile(mapping_, FILE_MAP_ALL_ACCESS, 0, 0, size);
|
view_ = MapViewOfFile(mapping_, FILE_MAP_ALL_ACCESS, 0, 0, size);
|
||||||
if (view_ == nullptr)
|
if (view_ == nullptr) {
|
||||||
{
|
|
||||||
CloseHandle(mapping_);
|
CloseHandle(mapping_);
|
||||||
mapping_ = nullptr;
|
mapping_ = nullptr;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -21,8 +21,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// WAVE_FORMAT_* tags we decode (kept local to avoid an mmreg.h dependency, matching audio_mix.hpp).
|
// WAVE_FORMAT_* tags we decode (kept local to avoid an mmreg.h dependency, matching audio_mix.hpp).
|
||||||
inline constexpr std::uint32_t kToneFormatPcm = 1;
|
inline constexpr std::uint32_t kToneFormatPcm = 1;
|
||||||
@@ -30,8 +29,7 @@ inline constexpr std::uint32_t kToneFormatFloat = 3;
|
|||||||
|
|
||||||
// One channel's worth of measured fidelity. Fields are NaN/0 when not applicable
|
// One channel's worth of measured fidelity. Fields are NaN/0 when not applicable
|
||||||
// (e.g. pitch metrics need a known expected_hz > 0).
|
// (e.g. pitch metrics need a known expected_hz > 0).
|
||||||
struct ToneReport
|
struct ToneReport {
|
||||||
{
|
|
||||||
bool valid = false; // enough samples to analyze
|
bool valid = false; // enough samples to analyze
|
||||||
unsigned sample_rate = 0; // the rate the samples are interpreted at (the *declared* rate)
|
unsigned sample_rate = 0; // the rate the samples are interpreted at (the *declared* rate)
|
||||||
std::size_t frames = 0; // mono frames analyzed
|
std::size_t frames = 0; // mono frames analyzed
|
||||||
@@ -59,8 +57,7 @@ struct ToneReport
|
|||||||
double dropout_ms = 0.0; // total duration of those gaps
|
double dropout_ms = 0.0; // total duration of those gaps
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace detail
|
namespace detail {
|
||||||
{
|
|
||||||
|
|
||||||
inline constexpr double kPi = 3.14159265358979323846;
|
inline constexpr double kPi = 3.14159265358979323846;
|
||||||
|
|
||||||
@@ -68,28 +65,22 @@ inline constexpr double kPi = 3.14159265358979323846;
|
|||||||
inline void fft(std::vector<std::complex<double>>& a)
|
inline void fft(std::vector<std::complex<double>>& a)
|
||||||
{
|
{
|
||||||
const std::size_t n = a.size();
|
const std::size_t n = a.size();
|
||||||
for (std::size_t i = 1, j = 0; i < n; ++i)
|
for (std::size_t i = 1, j = 0; i < n; ++i) {
|
||||||
{
|
|
||||||
std::size_t bit = n >> 1;
|
std::size_t bit = n >> 1;
|
||||||
for (; (j & bit) != 0; bit >>= 1)
|
for (; (j & bit) != 0; bit >>= 1) {
|
||||||
{
|
|
||||||
j ^= bit;
|
j ^= bit;
|
||||||
}
|
}
|
||||||
j ^= bit;
|
j ^= bit;
|
||||||
if (i < j)
|
if (i < j) {
|
||||||
{
|
|
||||||
std::swap(a[i], a[j]);
|
std::swap(a[i], a[j]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (std::size_t len = 2; len <= n; len <<= 1)
|
for (std::size_t len = 2; len <= n; len <<= 1) {
|
||||||
{
|
|
||||||
const double ang = -2.0 * kPi / static_cast<double>(len);
|
const double ang = -2.0 * kPi / static_cast<double>(len);
|
||||||
const std::complex<double> wlen(std::cos(ang), std::sin(ang));
|
const std::complex<double> wlen(std::cos(ang), std::sin(ang));
|
||||||
for (std::size_t i = 0; i < n; i += len)
|
for (std::size_t i = 0; i < n; i += len) {
|
||||||
{
|
|
||||||
std::complex<double> w(1.0, 0.0);
|
std::complex<double> w(1.0, 0.0);
|
||||||
for (std::size_t k = 0; k < len / 2; ++k)
|
for (std::size_t k = 0; k < len / 2; ++k) {
|
||||||
{
|
|
||||||
const std::complex<double> u = a[i + k];
|
const std::complex<double> u = a[i + k];
|
||||||
const std::complex<double> v = a[i + k + len / 2] * w;
|
const std::complex<double> v = a[i + k + len / 2] * w;
|
||||||
a[i + k] = u + v;
|
a[i + k] = u + v;
|
||||||
@@ -104,8 +95,7 @@ inline void fft(std::vector<std::complex<double>>& a)
|
|||||||
inline std::size_t floor_pow2(std::size_t n)
|
inline std::size_t floor_pow2(std::size_t n)
|
||||||
{
|
{
|
||||||
std::size_t p = 1;
|
std::size_t p = 1;
|
||||||
while ((p << 1) != 0 && (p << 1) <= n)
|
while ((p << 1) != 0 && (p << 1) <= n) {
|
||||||
{
|
|
||||||
p <<= 1;
|
p <<= 1;
|
||||||
}
|
}
|
||||||
return n == 0 ? 0 : p;
|
return n == 0 ? 0 : p;
|
||||||
@@ -120,27 +110,21 @@ inline std::vector<float> decode_channel(const std::uint8_t* pcm, std::size_t by
|
|||||||
std::uint32_t bits, std::uint32_t channels, std::uint32_t channel = 0)
|
std::uint32_t bits, std::uint32_t channels, std::uint32_t channel = 0)
|
||||||
{
|
{
|
||||||
std::vector<float> out;
|
std::vector<float> out;
|
||||||
if (pcm == nullptr || channels == 0 || channel >= channels)
|
if (pcm == nullptr || channels == 0 || channel >= channels) {
|
||||||
{
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
if (format_tag == kToneFormatFloat && bits == 32)
|
if (format_tag == kToneFormatFloat && bits == 32) {
|
||||||
{
|
|
||||||
const std::size_t frames = bytes / (channels * 4);
|
const std::size_t frames = bytes / (channels * 4);
|
||||||
out.reserve(frames);
|
out.reserve(frames);
|
||||||
const auto* f = reinterpret_cast<const float*>(pcm);
|
const auto* f = reinterpret_cast<const float*>(pcm);
|
||||||
for (std::size_t i = 0; i < frames; ++i)
|
for (std::size_t i = 0; i < frames; ++i) {
|
||||||
{
|
|
||||||
out.push_back(f[i * channels + channel]);
|
out.push_back(f[i * channels + channel]);
|
||||||
}
|
}
|
||||||
}
|
} else if (format_tag == kToneFormatPcm && bits == 16) {
|
||||||
else if (format_tag == kToneFormatPcm && bits == 16)
|
|
||||||
{
|
|
||||||
const std::size_t frames = bytes / (channels * 2);
|
const std::size_t frames = bytes / (channels * 2);
|
||||||
out.reserve(frames);
|
out.reserve(frames);
|
||||||
const auto* s = reinterpret_cast<const std::int16_t*>(pcm);
|
const auto* s = reinterpret_cast<const std::int16_t*>(pcm);
|
||||||
for (std::size_t i = 0; i < frames; ++i)
|
for (std::size_t i = 0; i < frames; ++i) {
|
||||||
{
|
|
||||||
out.push_back(static_cast<float>(s[i * channels + channel]) / 32768.0f);
|
out.push_back(static_cast<float>(s[i * channels + channel]) / 32768.0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,15 +134,13 @@ inline std::vector<float> decode_channel(const std::uint8_t* pcm, std::size_t by
|
|||||||
// Analyze a single channel of normalized float samples. `expected_hz` is the known
|
// Analyze a single channel of normalized float samples. `expected_hz` is the known
|
||||||
// input tone frequency (pass 0 to skip the pitch/SNR/THD metrics for non-tone audio;
|
// input tone frequency (pass 0 to skip the pitch/SNR/THD metrics for non-tone audio;
|
||||||
// the click/dropout/level metrics still apply).
|
// the click/dropout/level metrics still apply).
|
||||||
inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigned sample_rate,
|
inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigned sample_rate, double expected_hz)
|
||||||
double expected_hz)
|
|
||||||
{
|
{
|
||||||
ToneReport r;
|
ToneReport r;
|
||||||
r.sample_rate = sample_rate;
|
r.sample_rate = sample_rate;
|
||||||
r.frames = frames;
|
r.frames = frames;
|
||||||
r.expected_hz = expected_hz;
|
r.expected_hz = expected_hz;
|
||||||
if (samples == nullptr || frames < 64 || sample_rate == 0)
|
if (samples == nullptr || frames < 64 || sample_rate == 0) {
|
||||||
{
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
r.valid = true;
|
r.valid = true;
|
||||||
@@ -168,14 +150,12 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
|
|||||||
double sumsq = 0.0;
|
double sumsq = 0.0;
|
||||||
double peak = 0.0;
|
double peak = 0.0;
|
||||||
std::size_t clipped = 0;
|
std::size_t clipped = 0;
|
||||||
for (std::size_t i = 0; i < frames; ++i)
|
for (std::size_t i = 0; i < frames; ++i) {
|
||||||
{
|
|
||||||
const double x = samples[i];
|
const double x = samples[i];
|
||||||
sumsq += x * x;
|
sumsq += x * x;
|
||||||
const double a = std::fabs(x);
|
const double a = std::fabs(x);
|
||||||
peak = std::max(peak, a);
|
peak = std::max(peak, a);
|
||||||
if (a >= 0.999)
|
if (a >= 0.999) {
|
||||||
{
|
|
||||||
++clipped;
|
++clipped;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,11 +168,9 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
|
|||||||
// step. Use the median |first difference| as a robust scale (immune to the tone's own
|
// step. Use the median |first difference| as a robust scale (immune to the tone's own
|
||||||
// slope and to a few outliers), and flag steps beyond 8x it. Group samples within a
|
// slope and to a few outliers), and flag steps beyond 8x it. Group samples within a
|
||||||
// short refractory window into one event so a single click isn't counted many times.
|
// short refractory window into one event so a single click isn't counted many times.
|
||||||
if (frames >= 3)
|
if (frames >= 3) {
|
||||||
{
|
|
||||||
std::vector<float> diff(frames - 1);
|
std::vector<float> diff(frames - 1);
|
||||||
for (std::size_t i = 1; i < frames; ++i)
|
for (std::size_t i = 1; i < frames; ++i) {
|
||||||
{
|
|
||||||
diff[i - 1] = std::fabs(samples[i] - samples[i - 1]);
|
diff[i - 1] = std::fabs(samples[i] - samples[i - 1]);
|
||||||
}
|
}
|
||||||
std::vector<float> sorted(diff);
|
std::vector<float> sorted(diff);
|
||||||
@@ -202,12 +180,9 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
|
|||||||
const std::size_t refractory = std::max<std::size_t>(sample_rate / 1000, 8); // ~1 ms
|
const std::size_t refractory = std::max<std::size_t>(sample_rate / 1000, 8); // ~1 ms
|
||||||
std::size_t last_event_end = 0;
|
std::size_t last_event_end = 0;
|
||||||
bool have_event = false;
|
bool have_event = false;
|
||||||
for (std::size_t i = 0; i < diff.size(); ++i)
|
for (std::size_t i = 0; i < diff.size(); ++i) {
|
||||||
{
|
if (diff[i] > thresh) {
|
||||||
if (diff[i] > thresh)
|
if (!have_event || i > last_event_end) {
|
||||||
{
|
|
||||||
if (!have_event || i > last_event_end)
|
|
||||||
{
|
|
||||||
++r.glitch_count;
|
++r.glitch_count;
|
||||||
}
|
}
|
||||||
have_event = true;
|
have_event = true;
|
||||||
@@ -220,8 +195,7 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
|
|||||||
// --- Dropout detection: stretches that fall near-silent in an otherwise active signal ---
|
// --- Dropout detection: stretches that fall near-silent in an otherwise active signal ---
|
||||||
// Slide a ~5 ms window; flag windows whose RMS drops below 8% of the global RMS. Only
|
// Slide a ~5 ms window; flag windows whose RMS drops below 8% of the global RMS. Only
|
||||||
// meaningful when the signal is actually present (global RMS above a small floor).
|
// meaningful when the signal is actually present (global RMS above a small floor).
|
||||||
if (r.rms > 1e-4)
|
if (r.rms > 1e-4) {
|
||||||
{
|
|
||||||
const std::size_t win = std::max<std::size_t>(sample_rate * 5 / 1000, 16); // ~5 ms
|
const std::size_t win = std::max<std::size_t>(sample_rate * 5 / 1000, 16); // ~5 ms
|
||||||
const std::size_t hop = std::max<std::size_t>(win / 2, 1);
|
const std::size_t hop = std::max<std::size_t>(win / 2, 1);
|
||||||
const double silence_thresh = 0.08 * r.rms;
|
const double silence_thresh = 0.08 * r.rms;
|
||||||
@@ -230,33 +204,26 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
|
|||||||
std::size_t gap_last = 0; // last silent window's end sample
|
std::size_t gap_last = 0; // last silent window's end sample
|
||||||
std::size_t total_silent_samples = 0;
|
std::size_t total_silent_samples = 0;
|
||||||
auto close_gap = [&]() {
|
auto close_gap = [&]() {
|
||||||
if (in_gap)
|
if (in_gap) {
|
||||||
{
|
|
||||||
total_silent_samples += (gap_last - gap_first);
|
total_silent_samples += (gap_last - gap_first);
|
||||||
in_gap = false;
|
in_gap = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
for (std::size_t start = 0; start + win <= frames; start += hop)
|
for (std::size_t start = 0; start + win <= frames; start += hop) {
|
||||||
{
|
|
||||||
double ws = 0.0;
|
double ws = 0.0;
|
||||||
for (std::size_t i = 0; i < win; ++i)
|
for (std::size_t i = 0; i < win; ++i) {
|
||||||
{
|
|
||||||
const double x = samples[start + i];
|
const double x = samples[start + i];
|
||||||
ws += x * x;
|
ws += x * x;
|
||||||
}
|
}
|
||||||
const double wr = std::sqrt(ws / static_cast<double>(win));
|
const double wr = std::sqrt(ws / static_cast<double>(win));
|
||||||
if (wr < silence_thresh)
|
if (wr < silence_thresh) {
|
||||||
{
|
if (!in_gap) {
|
||||||
if (!in_gap)
|
|
||||||
{
|
|
||||||
++r.dropout_count;
|
++r.dropout_count;
|
||||||
gap_first = start;
|
gap_first = start;
|
||||||
in_gap = true;
|
in_gap = true;
|
||||||
}
|
}
|
||||||
gap_last = start + win;
|
gap_last = start + win;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
close_gap();
|
close_gap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,23 +234,19 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Spectral analysis (pitch / SNR / THD), Hann-windowed FFT ---
|
// --- Spectral analysis (pitch / SNR / THD), Hann-windowed FFT ---
|
||||||
if (expected_hz > 0.0)
|
if (expected_hz > 0.0) {
|
||||||
{
|
|
||||||
std::size_t n = detail::floor_pow2(frames);
|
std::size_t n = detail::floor_pow2(frames);
|
||||||
n = std::min<std::size_t>(n, std::size_t(1) << 18); // cap cost (~5 s @ 48k)
|
n = std::min<std::size_t>(n, std::size_t(1) << 18); // cap cost (~5 s @ 48k)
|
||||||
if (n >= 1024)
|
if (n >= 1024) {
|
||||||
{
|
|
||||||
std::vector<std::complex<double>> buf(n);
|
std::vector<std::complex<double>> buf(n);
|
||||||
for (std::size_t i = 0; i < n; ++i)
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
{
|
|
||||||
const double w = 0.5 - 0.5 * std::cos(2.0 * detail::kPi * i / (n - 1)); // Hann
|
const double w = 0.5 - 0.5 * std::cos(2.0 * detail::kPi * i / (n - 1)); // Hann
|
||||||
buf[i] = std::complex<double>(samples[i] * w, 0.0);
|
buf[i] = std::complex<double>(samples[i] * w, 0.0);
|
||||||
}
|
}
|
||||||
detail::fft(buf);
|
detail::fft(buf);
|
||||||
const std::size_t half = n / 2;
|
const std::size_t half = n / 2;
|
||||||
std::vector<double> mag(half);
|
std::vector<double> mag(half);
|
||||||
for (std::size_t i = 0; i < half; ++i)
|
for (std::size_t i = 0; i < half; ++i) {
|
||||||
{
|
|
||||||
mag[i] = std::abs(buf[i]);
|
mag[i] = std::abs(buf[i]);
|
||||||
}
|
}
|
||||||
const double bin_hz = static_cast<double>(sample_rate) / static_cast<double>(n);
|
const double bin_hz = static_cast<double>(sample_rate) / static_cast<double>(n);
|
||||||
@@ -291,31 +254,26 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
|
|||||||
// Peak bin, ignoring DC/very low bins (skip < 20 Hz).
|
// Peak bin, ignoring DC/very low bins (skip < 20 Hz).
|
||||||
std::size_t lo = std::max<std::size_t>(static_cast<std::size_t>(20.0 / bin_hz), 1);
|
std::size_t lo = std::max<std::size_t>(static_cast<std::size_t>(20.0 / bin_hz), 1);
|
||||||
std::size_t peak_bin = lo;
|
std::size_t peak_bin = lo;
|
||||||
for (std::size_t i = lo; i < half; ++i)
|
for (std::size_t i = lo; i < half; ++i) {
|
||||||
{
|
if (mag[i] > mag[peak_bin]) {
|
||||||
if (mag[i] > mag[peak_bin])
|
|
||||||
{
|
|
||||||
peak_bin = i;
|
peak_bin = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Quadratic (parabolic) interpolation on log-magnitude for a sub-bin estimate
|
// Quadratic (parabolic) interpolation on log-magnitude for a sub-bin estimate
|
||||||
// (accurate for a Hann-windowed peak).
|
// (accurate for a Hann-windowed peak).
|
||||||
double delta = 0.0;
|
double delta = 0.0;
|
||||||
if (peak_bin > 0 && peak_bin + 1 < half)
|
if (peak_bin > 0 && peak_bin + 1 < half) {
|
||||||
{
|
|
||||||
const double a = std::log(mag[peak_bin - 1] + 1e-30);
|
const double a = std::log(mag[peak_bin - 1] + 1e-30);
|
||||||
const double b = std::log(mag[peak_bin] + 1e-30);
|
const double b = std::log(mag[peak_bin] + 1e-30);
|
||||||
const double c = std::log(mag[peak_bin + 1] + 1e-30);
|
const double c = std::log(mag[peak_bin + 1] + 1e-30);
|
||||||
const double denom = (a - 2.0 * b + c);
|
const double denom = (a - 2.0 * b + c);
|
||||||
if (std::fabs(denom) > 1e-30)
|
if (std::fabs(denom) > 1e-30) {
|
||||||
{
|
|
||||||
delta = 0.5 * (a - c) / denom;
|
delta = 0.5 * (a - c) / denom;
|
||||||
delta = std::max(-0.5, std::min(0.5, delta));
|
delta = std::max(-0.5, std::min(0.5, delta));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.dominant_hz = (static_cast<double>(peak_bin) + delta) * bin_hz;
|
r.dominant_hz = (static_cast<double>(peak_bin) + delta) * bin_hz;
|
||||||
if (r.dominant_hz > 0.0)
|
if (r.dominant_hz > 0.0) {
|
||||||
{
|
|
||||||
r.pitch_error_ratio = r.dominant_hz / expected_hz;
|
r.pitch_error_ratio = r.dominant_hz / expected_hz;
|
||||||
r.pitch_error_cents = 1200.0 * std::log2(r.pitch_error_ratio);
|
r.pitch_error_cents = 1200.0 * std::log2(r.pitch_error_ratio);
|
||||||
}
|
}
|
||||||
@@ -328,10 +286,8 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
|
|||||||
auto lobe_power = [&](double hz, long half_w) {
|
auto lobe_power = [&](double hz, long half_w) {
|
||||||
const long center = static_cast<long>(std::lround(hz / bin_hz));
|
const long center = static_cast<long>(std::lround(hz / bin_hz));
|
||||||
double p = 0.0;
|
double p = 0.0;
|
||||||
for (long k = center - half_w; k <= center + half_w; ++k)
|
for (long k = center - half_w; k <= center + half_w; ++k) {
|
||||||
{
|
if (k >= 0 && static_cast<std::size_t>(k) < half) {
|
||||||
if (k >= 0 && static_cast<std::size_t>(k) < half)
|
|
||||||
{
|
|
||||||
p += mag[k] * mag[k];
|
p += mag[k] * mag[k];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,8 +295,7 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
|
|||||||
};
|
};
|
||||||
|
|
||||||
double total_power = 0.0;
|
double total_power = 0.0;
|
||||||
for (std::size_t i = lo; i < half; ++i)
|
for (std::size_t i = lo; i < half; ++i) {
|
||||||
{
|
|
||||||
total_power += mag[i] * mag[i];
|
total_power += mag[i] * mag[i];
|
||||||
}
|
}
|
||||||
const double fund_power = lobe_power(r.dominant_hz, 8);
|
const double fund_power = lobe_power(r.dominant_hz, 8);
|
||||||
@@ -348,11 +303,9 @@ inline ToneReport analyze_tone(const float* samples, std::size_t frames, unsigne
|
|||||||
r.snr_db = 10.0 * std::log10(std::max(fund_power, 1e-30) / residual);
|
r.snr_db = 10.0 * std::log10(std::max(fund_power, 1e-30) / residual);
|
||||||
|
|
||||||
double harm_power = 0.0;
|
double harm_power = 0.0;
|
||||||
for (int h = 2; h <= 6; ++h)
|
for (int h = 2; h <= 6; ++h) {
|
||||||
{
|
|
||||||
const double hz = r.dominant_hz * h;
|
const double hz = r.dominant_hz * h;
|
||||||
if (hz < (sample_rate / 2.0))
|
if (hz < (sample_rate / 2.0)) {
|
||||||
{
|
|
||||||
harm_power += lobe_power(hz, 3);
|
harm_power += lobe_power(hz, 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// Directory of the current executable, with a trailing separator.
|
// Directory of the current executable, with a trailing separator.
|
||||||
inline std::wstring exe_directory()
|
inline std::wstring exe_directory()
|
||||||
@@ -28,21 +27,17 @@ inline std::wstring exe_directory()
|
|||||||
inline std::wstring deployed_artifact_path(const wchar_t* name)
|
inline std::wstring deployed_artifact_path(const wchar_t* name)
|
||||||
{
|
{
|
||||||
const std::wstring here = exe_directory() + name;
|
const std::wstring here = exe_directory() + name;
|
||||||
if (GetFileAttributesW(here.c_str()) != INVALID_FILE_ATTRIBUTES)
|
if (GetFileAttributesW(here.c_str()) != INVALID_FILE_ATTRIBUTES) {
|
||||||
{
|
|
||||||
return here;
|
return here;
|
||||||
}
|
}
|
||||||
std::wstring dir = exe_directory();
|
std::wstring dir = exe_directory();
|
||||||
if (!dir.empty())
|
if (!dir.empty()) {
|
||||||
{
|
|
||||||
dir.pop_back(); // drop the trailing separator before going up a level
|
dir.pop_back(); // drop the trailing separator before going up a level
|
||||||
}
|
}
|
||||||
const std::size_t slash = dir.find_last_of(L"\\/");
|
const std::size_t slash = dir.find_last_of(L"\\/");
|
||||||
if (slash != std::wstring::npos)
|
if (slash != std::wstring::npos) {
|
||||||
{
|
|
||||||
const std::wstring up = dir.substr(0, slash + 1) + name;
|
const std::wstring up = dir.substr(0, slash + 1) + name;
|
||||||
if (GetFileAttributesW(up.c_str()) != INVALID_FILE_ATTRIBUTES)
|
if (GetFileAttributesW(up.c_str()) != INVALID_FILE_ATTRIBUTES) {
|
||||||
{
|
|
||||||
return up;
|
return up;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,9 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
struct WavData
|
struct WavData {
|
||||||
{
|
|
||||||
std::uint32_t sample_rate = 0;
|
std::uint32_t sample_rate = 0;
|
||||||
std::uint32_t channels = 0;
|
std::uint32_t channels = 0;
|
||||||
std::uint32_t bits = 0;
|
std::uint32_t bits = 0;
|
||||||
@@ -24,8 +22,7 @@ struct WavData
|
|||||||
std::vector<std::uint8_t> pcm; // interleaved frames
|
std::vector<std::uint8_t> pcm; // interleaved frames
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace detail
|
namespace detail {
|
||||||
{
|
|
||||||
inline void wav_put_u32(std::vector<std::uint8_t>& b, std::uint32_t v)
|
inline void wav_put_u32(std::vector<std::uint8_t>& b, std::uint32_t v)
|
||||||
{
|
{
|
||||||
b.push_back(v & 0xFF);
|
b.push_back(v & 0xFF);
|
||||||
@@ -75,12 +72,11 @@ inline bool wav_write(const std::wstring& path, const void* pcm, std::size_t byt
|
|||||||
detail::wav_put_u32(hdr, static_cast<std::uint32_t>(bytes));
|
detail::wav_put_u32(hdr, static_cast<std::uint32_t>(bytes));
|
||||||
|
|
||||||
FILE* f = nullptr;
|
FILE* f = nullptr;
|
||||||
if (_wfopen_s(&f, path.c_str(), L"wb") != 0 || f == nullptr)
|
if (_wfopen_s(&f, path.c_str(), L"wb") != 0 || f == nullptr) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const bool ok = std::fwrite(hdr.data(), 1, hdr.size(), f) == hdr.size() &&
|
const bool ok = std::fwrite(hdr.data(), 1, hdr.size(), f) == hdr.size()
|
||||||
(bytes == 0 || std::fwrite(pcm, 1, bytes, f) == bytes);
|
&& (bytes == 0 || std::fwrite(pcm, 1, bytes, f) == bytes);
|
||||||
std::fclose(f);
|
std::fclose(f);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
@@ -89,44 +85,37 @@ inline bool wav_write(const std::wstring& path, const void* pcm, std::size_t byt
|
|||||||
inline bool wav_read(const std::wstring& path, WavData& out)
|
inline bool wav_read(const std::wstring& path, WavData& out)
|
||||||
{
|
{
|
||||||
FILE* f = nullptr;
|
FILE* f = nullptr;
|
||||||
if (_wfopen_s(&f, path.c_str(), L"rb") != 0 || f == nullptr)
|
if (_wfopen_s(&f, path.c_str(), L"rb") != 0 || f == nullptr) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std::fseek(f, 0, SEEK_END);
|
std::fseek(f, 0, SEEK_END);
|
||||||
const long size = std::ftell(f);
|
const long size = std::ftell(f);
|
||||||
std::fseek(f, 0, SEEK_SET);
|
std::fseek(f, 0, SEEK_SET);
|
||||||
if (size < 44)
|
if (size < 44) {
|
||||||
{
|
|
||||||
std::fclose(f);
|
std::fclose(f);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std::vector<std::uint8_t> all(static_cast<std::size_t>(size));
|
std::vector<std::uint8_t> all(static_cast<std::size_t>(size));
|
||||||
const bool read_ok = std::fread(all.data(), 1, all.size(), f) == all.size();
|
const bool read_ok = std::fread(all.data(), 1, all.size(), f) == all.size();
|
||||||
std::fclose(f);
|
std::fclose(f);
|
||||||
if (!read_ok || std::memcmp(all.data(), "RIFF", 4) != 0 || std::memcmp(all.data() + 8, "WAVE", 4) != 0)
|
if (!read_ok || std::memcmp(all.data(), "RIFF", 4) != 0 || std::memcmp(all.data() + 8, "WAVE", 4) != 0) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk chunks for "fmt " and "data".
|
// Walk chunks for "fmt " and "data".
|
||||||
std::size_t pos = 12;
|
std::size_t pos = 12;
|
||||||
bool have_fmt = false, have_data = false;
|
bool have_fmt = false, have_data = false;
|
||||||
while (pos + 8 <= all.size())
|
while (pos + 8 <= all.size()) {
|
||||||
{
|
|
||||||
const std::uint8_t* p = all.data() + pos;
|
const std::uint8_t* p = all.data() + pos;
|
||||||
const std::uint32_t chunk_size = detail::wav_get_u32(p + 4);
|
const std::uint32_t chunk_size = detail::wav_get_u32(p + 4);
|
||||||
const std::size_t body = pos + 8;
|
const std::size_t body = pos + 8;
|
||||||
if (std::memcmp(p, "fmt ", 4) == 0 && body + 16 <= all.size())
|
if (std::memcmp(p, "fmt ", 4) == 0 && body + 16 <= all.size()) {
|
||||||
{
|
|
||||||
out.format_tag = detail::wav_get_u16(all.data() + body + 0);
|
out.format_tag = detail::wav_get_u16(all.data() + body + 0);
|
||||||
out.channels = detail::wav_get_u16(all.data() + body + 2);
|
out.channels = detail::wav_get_u16(all.data() + body + 2);
|
||||||
out.sample_rate = detail::wav_get_u32(all.data() + body + 4);
|
out.sample_rate = detail::wav_get_u32(all.data() + body + 4);
|
||||||
out.bits = detail::wav_get_u16(all.data() + body + 14);
|
out.bits = detail::wav_get_u16(all.data() + body + 14);
|
||||||
have_fmt = true;
|
have_fmt = true;
|
||||||
}
|
} else if (std::memcmp(p, "data", 4) == 0) {
|
||||||
else if (std::memcmp(p, "data", 4) == 0)
|
|
||||||
{
|
|
||||||
const std::size_t avail = all.size() - body;
|
const std::size_t avail = all.size() - body;
|
||||||
const std::size_t n = std::min<std::size_t>(chunk_size, avail);
|
const std::size_t n = std::min<std::size_t>(chunk_size, avail);
|
||||||
out.pcm.assign(all.begin() + body, all.begin() + body + n);
|
out.pcm.assign(all.begin() + body, all.begin() + body + n);
|
||||||
@@ -136,8 +125,7 @@ inline bool wav_read(const std::wstring& path, WavData& out)
|
|||||||
// wrap `pos` on a 32-bit size_t (x86) and spin the loop on garbage, and there's nothing valid
|
// wrap `pos` on a 32-bit size_t (x86) and spin the loop on garbage, and there's nothing valid
|
||||||
// past a chunk that claims more than the file holds anyway.
|
// past a chunk that claims more than the file holds anyway.
|
||||||
const std::size_t advance = static_cast<std::size_t>(chunk_size) + (chunk_size & 1);
|
const std::size_t advance = static_cast<std::size_t>(chunk_size) + (chunk_size & 1);
|
||||||
if (advance > all.size() - body)
|
if (advance > all.size() - body) {
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
pos = body + advance;
|
pos = body + advance;
|
||||||
|
|||||||
@@ -17,11 +17,9 @@
|
|||||||
#include "rate_estimator.hpp"
|
#include "rate_estimator.hpp"
|
||||||
#include "vtable_hook.hpp"
|
#include "vtable_hook.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
// COM vtable indices (frozen ABI). IUnknown occupies 0..2.
|
// COM vtable indices (frozen ABI). IUnknown occupies 0..2.
|
||||||
// IMMDevice: Activate = 3
|
// IMMDevice: Activate = 3
|
||||||
@@ -50,8 +48,7 @@ using ReleaseBufferFn = HRESULT(STDMETHODCALLTYPE*)(IAudioRenderClient*, UINT32,
|
|||||||
// Swapping the slot leaves the original code untouched.
|
// Swapping the slot leaves the original code untouched.
|
||||||
|
|
||||||
// The scalar audio format we forward; resolved from the game's WAVEFORMATEX.
|
// The scalar audio format we forward; resolved from the game's WAVEFORMATEX.
|
||||||
struct CapturedFormat
|
struct CapturedFormat {
|
||||||
{
|
|
||||||
std::uint32_t rate = 0;
|
std::uint32_t rate = 0;
|
||||||
std::uint32_t channels = 0;
|
std::uint32_t channels = 0;
|
||||||
std::uint32_t bits = 0;
|
std::uint32_t bits = 0;
|
||||||
@@ -126,8 +123,7 @@ CapturedFormat g_stream_formats[kMaxAudioStreams];
|
|||||||
std::unordered_map<IAudioClient*, CapturedFormat> g_client_formats;
|
std::unordered_map<IAudioClient*, CapturedFormat> g_client_formats;
|
||||||
|
|
||||||
// Streams we track (frame counting + per-stream capture). Index 0 is primary.
|
// Streams we track (frame counting + per-stream capture). Index 0 is primary.
|
||||||
struct TrackedStream
|
struct TrackedStream {
|
||||||
{
|
|
||||||
std::atomic<IAudioRenderClient*> client{nullptr};
|
std::atomic<IAudioRenderClient*> client{nullptr};
|
||||||
std::atomic<std::uint64_t> frames{0};
|
std::atomic<std::uint64_t> frames{0};
|
||||||
std::atomic<std::uint32_t> block_align{0}; // hot-path frame size for this stream
|
std::atomic<std::uint32_t> block_align{0}; // hot-path frame size for this stream
|
||||||
@@ -183,20 +179,15 @@ CapturedFormat capture_format(const WAVEFORMATEX* wfx)
|
|||||||
cf.bits = wfx->wBitsPerSample;
|
cf.bits = wfx->wBitsPerSample;
|
||||||
cf.block_align = wfx->nBlockAlign;
|
cf.block_align = wfx->nBlockAlign;
|
||||||
cf.tag = wfx->wFormatTag;
|
cf.tag = wfx->wFormatTag;
|
||||||
if (wfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE && wfx->cbSize >= 22)
|
if (wfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE && wfx->cbSize >= 22) {
|
||||||
{
|
|
||||||
const auto* ext = reinterpret_cast<const WAVEFORMATEXTENSIBLE*>(wfx);
|
const auto* ext = reinterpret_cast<const WAVEFORMATEXTENSIBLE*>(wfx);
|
||||||
if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)
|
if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {
|
||||||
{
|
|
||||||
cf.tag = WAVE_FORMAT_IEEE_FLOAT;
|
cf.tag = WAVE_FORMAT_IEEE_FLOAT;
|
||||||
}
|
} else if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_PCM) {
|
||||||
else if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_PCM)
|
|
||||||
{
|
|
||||||
cf.tag = WAVE_FORMAT_PCM;
|
cf.tag = WAVE_FORMAT_PCM;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cf.block_align == 0)
|
if (cf.block_align == 0) {
|
||||||
{
|
|
||||||
cf.block_align = cf.channels * (cf.bits / 8);
|
cf.block_align = cf.channels * (cf.bits / 8);
|
||||||
}
|
}
|
||||||
return cf;
|
return cf;
|
||||||
@@ -214,13 +205,10 @@ void try_register_lazy(IAudioRenderClient* rc);
|
|||||||
std::uint32_t readable_bytes(const void* ptr, std::uint32_t want)
|
std::uint32_t readable_bytes(const void* ptr, std::uint32_t want)
|
||||||
{
|
{
|
||||||
MEMORY_BASIC_INFORMATION mbi{};
|
MEMORY_BASIC_INFORMATION mbi{};
|
||||||
if (VirtualQuery(ptr, &mbi, sizeof(mbi)) == sizeof(mbi) && mbi.State == MEM_COMMIT)
|
if (VirtualQuery(ptr, &mbi, sizeof(mbi)) == sizeof(mbi) && mbi.State == MEM_COMMIT) {
|
||||||
{
|
|
||||||
const auto* base = static_cast<const std::uint8_t*>(mbi.BaseAddress);
|
const auto* base = static_cast<const std::uint8_t*>(mbi.BaseAddress);
|
||||||
const auto avail = static_cast<std::uintptr_t>((base + mbi.RegionSize) -
|
const auto avail = static_cast<std::uintptr_t>((base + mbi.RegionSize) - static_cast<const std::uint8_t*>(ptr));
|
||||||
static_cast<const std::uint8_t*>(ptr));
|
if (avail < want) {
|
||||||
if (avail < want)
|
|
||||||
{
|
|
||||||
return static_cast<std::uint32_t>(avail);
|
return static_cast<std::uint32_t>(avail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,8 +220,7 @@ HRESULT STDMETHODCALLTYPE hk_GetBuffer(IAudioRenderClient* self, UINT32 num_fram
|
|||||||
DetourGate::Guard guard(g_gate); // in-flight until return (drained before an unhook tears down)
|
DetourGate::Guard guard(g_gate); // in-flight until return (drained before an unhook tears down)
|
||||||
hook_note_call(g_id_getbuffer);
|
hook_note_call(g_id_getbuffer);
|
||||||
const HRESULT hr = g_vh_getbuffer.original<GetBufferFn>()(self, num_frames, data);
|
const HRESULT hr = g_vh_getbuffer.original<GetBufferFn>()(self, num_frames, data);
|
||||||
if (SUCCEEDED(hr) && data != nullptr)
|
if (SUCCEEDED(hr) && data != nullptr) {
|
||||||
{
|
|
||||||
t_gb_client = self;
|
t_gb_client = self;
|
||||||
t_gb_data = *data;
|
t_gb_data = *data;
|
||||||
t_gb_frames = num_frames;
|
t_gb_frames = num_frames;
|
||||||
@@ -249,37 +236,32 @@ HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_
|
|||||||
// A render client we've never seen actively rendering is almost certainly one
|
// A render client we've never seen actively rendering is almost certainly one
|
||||||
// the game created before we injected; adopt it now (the first becomes the
|
// the game created before we injected; adopt it now (the first becomes the
|
||||||
// primary we capture). Skip our own silent probe client.
|
// primary we capture). Skip our own silent probe client.
|
||||||
if (self != g_self_render.load(std::memory_order_acquire) && num_frames > 0 && !stream_tracked(self))
|
if (self != g_self_render.load(std::memory_order_acquire) && num_frames > 0 && !stream_tracked(self)) {
|
||||||
{
|
|
||||||
try_register_lazy(self);
|
try_register_lazy(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per tracked stream: count frames (debug view) and, into the stream's own ring,
|
// Per tracked stream: count frames (debug view) and, into the stream's own ring,
|
||||||
// capture + silence its buffer while capture is enabled. Every stream is captured
|
// capture + silence its buffer while capture is enabled. Every stream is captured
|
||||||
// into its own ring; the host mixes them.
|
// into its own ring; the host mixes them.
|
||||||
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i)
|
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i) {
|
||||||
{
|
if (g_streams[i].client.load(std::memory_order_acquire) != self) {
|
||||||
if (g_streams[i].client.load(std::memory_order_acquire) != self)
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const std::uint64_t total =
|
const std::uint64_t total = g_streams[i].frames.fetch_add(num_frames, std::memory_order_relaxed) + num_frames;
|
||||||
g_streams[i].frames.fetch_add(num_frames, std::memory_order_relaxed) + num_frames;
|
|
||||||
if (IpcClient* ipc = g_ipc.load(std::memory_order_acquire)) // load once (unhook may null it)
|
if (IpcClient* ipc = g_ipc.load(std::memory_order_acquire)) // load once (unhook may null it)
|
||||||
{
|
{
|
||||||
ipc->note_audio_frames(i, total);
|
ipc->note_audio_frames(i, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (num_frames > 0 && (flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0)
|
if (num_frames > 0 && (flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
|
||||||
{
|
|
||||||
AudioRingHeader* ring = g_rings[i].load(std::memory_order_acquire);
|
AudioRingHeader* ring = g_rings[i].load(std::memory_order_acquire);
|
||||||
// Only capture once the format is published -- for a guessed-rate stream that's
|
// Only capture once the format is published -- for a guessed-rate stream that's
|
||||||
// after the true rate is measured, so we never capture/silence audio we'd
|
// after the true rate is measured, so we never capture/silence audio we'd
|
||||||
// mis-rate (and don't build a backlog while measuring; the game stays audible).
|
// mis-rate (and don't build a backlog while measuring; the game stays audible).
|
||||||
if (ring != nullptr && ring->capture_enabled.load(std::memory_order_relaxed) != 0 &&
|
if (ring != nullptr && ring->capture_enabled.load(std::memory_order_relaxed) != 0
|
||||||
audio_ring_format_ready(*ring) && t_gb_client == self && t_gb_data != nullptr &&
|
&& audio_ring_format_ready(*ring) && t_gb_client == self && t_gb_data != nullptr
|
||||||
t_gb_frames == num_frames &&
|
&& t_gb_frames == num_frames
|
||||||
t_gb_epoch == g_hook_epoch.load(std::memory_order_acquire)) // same hooked epoch as the GetBuffer
|
&& t_gb_epoch == g_hook_epoch.load(std::memory_order_acquire)) // same hooked epoch as the GetBuffer
|
||||||
{
|
{
|
||||||
const bool guessed = g_streams[i].assumed_format.load(std::memory_order_relaxed) != 0;
|
const bool guessed = g_streams[i].assumed_format.load(std::memory_order_relaxed) != 0;
|
||||||
const std::uint32_t block = g_streams[i].block_align.load(std::memory_order_relaxed);
|
const std::uint32_t block = g_streams[i].block_align.load(std::memory_order_relaxed);
|
||||||
@@ -287,15 +269,13 @@ HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_
|
|||||||
// A guessed (pre-existing client) stream's block may be larger than the real
|
// A guessed (pre-existing client) stream's block may be larger than the real
|
||||||
// per-frame size, so clamp the COPY to what's actually readable -- never over-read
|
// per-frame size, so clamp the COPY to what's actually readable -- never over-read
|
||||||
// the game's buffer (no-op when the guess is right).
|
// the game's buffer (no-op when the guess is right).
|
||||||
if (guessed)
|
if (guessed) {
|
||||||
{
|
|
||||||
bytes = readable_bytes(t_gb_data, bytes);
|
bytes = readable_bytes(t_gb_data, bytes);
|
||||||
}
|
}
|
||||||
// Only silence if the frames made it into the ring; if the host has
|
// Only silence if the frames made it into the ring; if the host has
|
||||||
// stalled (ring full) keep playing locally rather than going dead
|
// stalled (ring full) keep playing locally rather than going dead
|
||||||
// silent — degrades to today's echo, never to silence.
|
// silent — degrades to today's echo, never to silence.
|
||||||
if (block != 0 && audio_ring_push(*ring, t_gb_data, bytes, num_frames))
|
if (block != 0 && audio_ring_push(*ring, t_gb_data, bytes, num_frames)) {
|
||||||
{
|
|
||||||
g_frames_captured.fetch_add(num_frames, std::memory_order_relaxed);
|
g_frames_captured.fetch_add(num_frames, std::memory_order_relaxed);
|
||||||
// Mute the game's local playback so the only audio is the host's re-render.
|
// Mute the game's local playback so the only audio is the host's re-render.
|
||||||
// Otherwise the game plays locally AND the mirror re-renders the same audio a
|
// Otherwise the game plays locally AND the mirror re-renders the same audio a
|
||||||
@@ -306,13 +286,12 @@ HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_
|
|||||||
// format we additionally zero the buffer (belt-and-suspenders; `block` is the
|
// format we additionally zero the buffer (belt-and-suspenders; `block` is the
|
||||||
// real frame size there, so it stays in-bounds). Only mutes once the frames made
|
// real frame size there, so it stays in-bounds). Only mutes once the frames made
|
||||||
// the ring (above) -- a stalled host degrades to echo, never to dead silence.
|
// the ring (above) -- a stalled host degrades to echo, never to dead silence.
|
||||||
if (!guessed)
|
if (!guessed) {
|
||||||
{
|
|
||||||
std::memset(t_gb_data, 0, bytes);
|
std::memset(t_gb_data, 0, bytes);
|
||||||
}
|
}
|
||||||
g_frames_silenced.fetch_add(num_frames, std::memory_order_relaxed);
|
g_frames_silenced.fetch_add(num_frames, std::memory_order_relaxed);
|
||||||
return g_vh_releasebuffer.original<ReleaseBufferFn>()(
|
return g_vh_releasebuffer.original<ReleaseBufferFn>()(self, num_frames,
|
||||||
self, num_frames, flags | AUDCLNT_BUFFERFLAGS_SILENT);
|
flags | AUDCLNT_BUFFERFLAGS_SILENT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,18 +302,14 @@ HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_
|
|||||||
// audio and cross-correlate them to recover the true format from ground truth. The game
|
// audio and cross-correlate them to recover the true format from ground truth. The game
|
||||||
// stays audible (the host runs loopback during the measurement window anyway), and the
|
// stays audible (the host runs loopback during the measurement window anyway), and the
|
||||||
// shipping no-echo capture/silence path above is left completely untouched.
|
// shipping no-echo capture/silence path above is left completely untouched.
|
||||||
if (num_frames > 0 && (flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0)
|
if (num_frames > 0 && (flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
|
||||||
{
|
|
||||||
AudioRingHeader* vring = g_rings[i].load(std::memory_order_acquire);
|
AudioRingHeader* vring = g_rings[i].load(std::memory_order_acquire);
|
||||||
if (vring != nullptr && vring->verify_capture.load(std::memory_order_relaxed) != 0 &&
|
if (vring != nullptr && vring->verify_capture.load(std::memory_order_relaxed) != 0
|
||||||
!audio_ring_format_ready(*vring) &&
|
&& !audio_ring_format_ready(*vring) && g_streams[i].assumed_format.load(std::memory_order_relaxed) != 0
|
||||||
g_streams[i].assumed_format.load(std::memory_order_relaxed) != 0 && t_gb_client == self &&
|
&& t_gb_client == self && t_gb_data != nullptr && t_gb_frames == num_frames
|
||||||
t_gb_data != nullptr && t_gb_frames == num_frames &&
|
&& t_gb_epoch == g_hook_epoch.load(std::memory_order_acquire)) {
|
||||||
t_gb_epoch == g_hook_epoch.load(std::memory_order_acquire))
|
|
||||||
{
|
|
||||||
const std::uint32_t block = g_streams[i].block_align.load(std::memory_order_relaxed);
|
const std::uint32_t block = g_streams[i].block_align.load(std::memory_order_relaxed);
|
||||||
if (block != 0)
|
if (block != 0) {
|
||||||
{
|
|
||||||
// Self-describing chunk: [u32 frame-count][num_frames*block bytes]. The host can't
|
// Self-describing chunk: [u32 frame-count][num_frames*block bytes]. The host can't
|
||||||
// know the real frame size of a guessed stream, so it recovers the layout by
|
// know the real frame size of a guessed stream, so it recovers the layout by
|
||||||
// trying candidate de-interleavings -- but it needs the frame count to strip the
|
// trying candidate de-interleavings -- but it needs the frame count to strip the
|
||||||
@@ -342,9 +317,8 @@ HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_
|
|||||||
// channels/bits). Push both parts only if both fit and the payload is fully
|
// channels/bits). Push both parts only if both fit and the payload is fully
|
||||||
// readable, so a full ring or a short buffer can never tear the framing.
|
// readable, so a full ring or a short buffer can never tear the framing.
|
||||||
const std::uint32_t want = num_frames * block;
|
const std::uint32_t want = num_frames * block;
|
||||||
if (readable_bytes(t_gb_data, want) == want &&
|
if (readable_bytes(t_gb_data, want) == want
|
||||||
audio_ring_free_space(*vring) >= static_cast<std::uint32_t>(sizeof(num_frames)) + want)
|
&& audio_ring_free_space(*vring) >= static_cast<std::uint32_t>(sizeof(num_frames)) + want) {
|
||||||
{
|
|
||||||
audio_ring_push(*vring, &num_frames, sizeof(num_frames), 0);
|
audio_ring_push(*vring, &num_frames, sizeof(num_frames), 0);
|
||||||
audio_ring_push(*vring, t_gb_data, want, num_frames);
|
audio_ring_push(*vring, t_gb_data, want, num_frames);
|
||||||
}
|
}
|
||||||
@@ -358,12 +332,10 @@ HRESULT STDMETHODCALLTYPE hk_ReleaseBuffer(IAudioRenderClient* self, UINT32 num_
|
|||||||
|
|
||||||
// Publish a stream's format + state to the host's per-stream debug channel. Caller holds
|
// Publish a stream's format + state to the host's per-stream debug channel. Caller holds
|
||||||
// g_setup_mutex.
|
// g_setup_mutex.
|
||||||
void publish_stream_info_locked(std::uint32_t slot, const CapturedFormat& cf, std::uint32_t state,
|
void publish_stream_info_locked(std::uint32_t slot, const CapturedFormat& cf, std::uint32_t state, std::uint64_t frames)
|
||||||
std::uint64_t frames)
|
|
||||||
{
|
{
|
||||||
IpcClient* ipc = g_ipc.load(std::memory_order_acquire);
|
IpcClient* ipc = g_ipc.load(std::memory_order_acquire);
|
||||||
if (ipc == nullptr)
|
if (ipc == nullptr) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AudioStreamInfo info{};
|
AudioStreamInfo info{};
|
||||||
@@ -384,35 +356,28 @@ void publish_stream_info_locked(std::uint32_t slot, const CapturedFormat& cf, st
|
|||||||
bool publish_stream_format_locked(std::uint32_t slot)
|
bool publish_stream_format_locked(std::uint32_t slot)
|
||||||
{
|
{
|
||||||
AudioRingHeader* ring = g_rings[slot].load(std::memory_order_acquire);
|
AudioRingHeader* ring = g_rings[slot].load(std::memory_order_acquire);
|
||||||
if (ring == nullptr || g_stream_formats[slot].rate == 0)
|
if (ring == nullptr || g_stream_formats[slot].rate == 0) {
|
||||||
{
|
|
||||||
return false; // no ring attached yet, or no stream in this slot
|
return false; // no ring attached yet, or no stream in this slot
|
||||||
}
|
}
|
||||||
if (audio_ring_format_ready(*ring))
|
if (audio_ring_format_ready(*ring)) {
|
||||||
{
|
|
||||||
return true; // already published
|
return true; // already published
|
||||||
}
|
}
|
||||||
CapturedFormat cf = g_stream_formats[slot];
|
CapturedFormat cf = g_stream_formats[slot];
|
||||||
if (g_stream_rate_guess[slot])
|
if (g_stream_rate_guess[slot]) {
|
||||||
{
|
|
||||||
// Feed this tick's render cadence to the estimator; it only commits on consensus
|
// Feed this tick's render cadence to the estimator; it only commits on consensus
|
||||||
// across standard-rate windows, or a low-confidence fallback after enough attempts.
|
// across standard-rate windows, or a low-confidence fallback after enough attempts.
|
||||||
LARGE_INTEGER now{}, freq{};
|
LARGE_INTEGER now{}, freq{};
|
||||||
QueryPerformanceCounter(&now);
|
QueryPerformanceCounter(&now);
|
||||||
QueryPerformanceFrequency(&freq);
|
QueryPerformanceFrequency(&freq);
|
||||||
const RateEstimate est = g_rate_estimator[slot].feed(
|
const RateEstimate est = g_rate_estimator[slot].feed(g_streams[slot].frames.load(std::memory_order_relaxed),
|
||||||
g_streams[slot].frames.load(std::memory_order_relaxed), now.QuadPart, freq.QuadPart);
|
now.QuadPart, freq.QuadPart);
|
||||||
if (!est.done)
|
if (!est.done) {
|
||||||
{
|
|
||||||
return false; // still measuring; caller retries next tick
|
return false; // still measuring; caller retries next tick
|
||||||
}
|
}
|
||||||
const std::uint32_t state = est.confident ? AudioFormat_Measured : AudioFormat_LowConfidence;
|
const std::uint32_t state = est.confident ? AudioFormat_Measured : AudioFormat_LowConfidence;
|
||||||
if (est.confident)
|
if (est.confident) {
|
||||||
{
|
|
||||||
logf("audio stream %u: measured rate %uHz (was guessing %uHz)", slot, est.rate, cf.rate);
|
logf("audio stream %u: measured rate %uHz (was guessing %uHz)", slot, est.rate, cf.rate);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
logw("audio stream %u: rate %uHz is a LOW-CONFIDENCE estimate (no consensus) -- verify or "
|
logw("audio stream %u: rate %uHz is a LOW-CONFIDENCE estimate (no consensus) -- verify or "
|
||||||
"override",
|
"override",
|
||||||
slot, est.rate);
|
slot, est.rate);
|
||||||
@@ -435,12 +400,10 @@ bool publish_stream_format_locked(std::uint32_t slot)
|
|||||||
void apply_audio_op_locked(std::uint32_t slot, const AudioRingOpCmd& cmd)
|
void apply_audio_op_locked(std::uint32_t slot, const AudioRingOpCmd& cmd)
|
||||||
{
|
{
|
||||||
AudioRingHeader* ring = g_rings[slot].load(std::memory_order_acquire);
|
AudioRingHeader* ring = g_rings[slot].load(std::memory_order_acquire);
|
||||||
if (ring == nullptr || g_stream_formats[slot].rate == 0)
|
if (ring == nullptr || g_stream_formats[slot].rate == 0) {
|
||||||
{
|
|
||||||
return; // no ring / no stream in this slot
|
return; // no ring / no stream in this slot
|
||||||
}
|
}
|
||||||
if (cmd.kind == AudioRingOp_Remeasure)
|
if (cmd.kind == AudioRingOp_Remeasure) {
|
||||||
{
|
|
||||||
logw("audio stream %u: operator requested re-measure", slot);
|
logw("audio stream %u: operator requested re-measure", slot);
|
||||||
g_stream_rate_guess[slot] = true;
|
g_stream_rate_guess[slot] = true;
|
||||||
g_rate_estimator[slot] = RateEstimator{};
|
g_rate_estimator[slot] = RateEstimator{};
|
||||||
@@ -450,23 +413,19 @@ void apply_audio_op_locked(std::uint32_t slot, const AudioRingOpCmd& cmd)
|
|||||||
ring->format_valid.store(0, std::memory_order_release); // force re-publish after measuring
|
ring->format_valid.store(0, std::memory_order_release); // force re-publish after measuring
|
||||||
publish_stream_info_locked(slot, g_stream_formats[slot], AudioFormat_Measuring,
|
publish_stream_info_locked(slot, g_stream_formats[slot], AudioFormat_Measuring,
|
||||||
g_streams[slot].frames.load(std::memory_order_relaxed));
|
g_streams[slot].frames.load(std::memory_order_relaxed));
|
||||||
}
|
} else if (cmd.kind == AudioRingOp_Override) {
|
||||||
else if (cmd.kind == AudioRingOp_Override)
|
|
||||||
{
|
|
||||||
CapturedFormat cf;
|
CapturedFormat cf;
|
||||||
cf.rate = cmd.rate;
|
cf.rate = cmd.rate;
|
||||||
cf.channels = cmd.channels;
|
cf.channels = cmd.channels;
|
||||||
cf.bits = cmd.bits;
|
cf.bits = cmd.bits;
|
||||||
cf.tag = cmd.format_tag ? cmd.format_tag : WAVE_FORMAT_PCM;
|
cf.tag = cmd.format_tag ? cmd.format_tag : WAVE_FORMAT_PCM;
|
||||||
cf.block_align = cmd.channels * (cmd.bits / 8);
|
cf.block_align = cmd.channels * (cmd.bits / 8);
|
||||||
if (cf.rate == 0 || cf.channels == 0 || cf.block_align == 0)
|
if (cf.rate == 0 || cf.channels == 0 || cf.block_align == 0) {
|
||||||
{
|
logw("audio stream %u: ignoring invalid override %uHz/%uch/%ubit", slot, cf.rate, cf.channels, cf.bits);
|
||||||
logw("audio stream %u: ignoring invalid override %uHz/%uch/%ubit", slot, cf.rate, cf.channels,
|
|
||||||
cf.bits);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logw("audio stream %u: operator override -> %uHz/%uch/%ubit tag=%u", slot, cf.rate, cf.channels,
|
logw("audio stream %u: operator override -> %uHz/%uch/%ubit tag=%u", slot, cf.rate, cf.channels, cf.bits,
|
||||||
cf.bits, cf.tag);
|
cf.tag);
|
||||||
g_stream_formats[slot] = cf;
|
g_stream_formats[slot] = cf;
|
||||||
g_stream_rate_guess[slot] = false;
|
g_stream_rate_guess[slot] = false;
|
||||||
g_stream_format_state[slot] = AudioFormat_Override;
|
g_stream_format_state[slot] = AudioFormat_Override;
|
||||||
@@ -487,25 +446,21 @@ void apply_audio_op_locked(std::uint32_t slot, const AudioRingOpCmd& cmd)
|
|||||||
// the format is published). Caller holds g_setup_mutex.
|
// the format is published). Caller holds g_setup_mutex.
|
||||||
void register_render_client_locked(IAudioRenderClient* rc, const CapturedFormat& cf, bool rate_is_guess)
|
void register_render_client_locked(IAudioRenderClient* rc, const CapturedFormat& cf, bool rate_is_guess)
|
||||||
{
|
{
|
||||||
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i)
|
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i) {
|
||||||
{
|
if (g_streams[i].client.load(std::memory_order_relaxed) == rc) {
|
||||||
if (g_streams[i].client.load(std::memory_order_relaxed) == rc)
|
|
||||||
{
|
|
||||||
return; // already tracked
|
return; // already tracked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::uint32_t seen = g_streams_seen.fetch_add(1, std::memory_order_relaxed) + 1;
|
const std::uint32_t seen = g_streams_seen.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||||
if (IpcClient* ipc = g_ipc.load(std::memory_order_acquire))
|
if (IpcClient* ipc = g_ipc.load(std::memory_order_acquire)) {
|
||||||
{
|
|
||||||
ipc->set_audio_streams_seen(seen);
|
ipc->set_audio_streams_seen(seen);
|
||||||
}
|
}
|
||||||
logf("register_render_client: rc=%p seen=%u fmt=%uHz/%uch/%ubit tag=%u block=%u", rc, seen, cf.rate,
|
logf("register_render_client: rc=%p seen=%u fmt=%uHz/%uch/%ubit tag=%u block=%u", rc, seen, cf.rate, cf.channels,
|
||||||
cf.channels, cf.bits, cf.tag, cf.block_align);
|
cf.bits, cf.tag, cf.block_align);
|
||||||
|
|
||||||
const std::uint32_t slot = g_registered;
|
const std::uint32_t slot = g_registered;
|
||||||
if (slot >= kMaxAudioStreams)
|
if (slot >= kMaxAudioStreams) {
|
||||||
{
|
|
||||||
return; // more streams than debug slots; counted above, not detailed
|
return; // more streams than debug slots; counted above, not detailed
|
||||||
}
|
}
|
||||||
g_registered = slot + 1;
|
g_registered = slot + 1;
|
||||||
@@ -520,16 +475,13 @@ void register_render_client_locked(IAudioRenderClient* rc, const CapturedFormat&
|
|||||||
g_streams[slot].block_align.store(cf.block_align, std::memory_order_relaxed); // before client (hot path)
|
g_streams[slot].block_align.store(cf.block_align, std::memory_order_relaxed); // before client (hot path)
|
||||||
g_streams[slot].client.store(rc, std::memory_order_release);
|
g_streams[slot].client.store(rc, std::memory_order_release);
|
||||||
|
|
||||||
if (rate_is_guess)
|
if (rate_is_guess) {
|
||||||
{
|
|
||||||
logf("audio stream %u: format unknown (pre-existing client) -> assuming device mix %uHz/%uch/%ubit; "
|
logf("audio stream %u: format unknown (pre-existing client) -> assuming device mix %uHz/%uch/%ubit; "
|
||||||
"measuring true rate; channels/bits assumed (verified byte-compatible before capture)",
|
"measuring true rate; channels/bits assumed (verified byte-compatible before capture)",
|
||||||
slot, cf.rate, cf.channels, cf.bits);
|
slot, cf.rate, cf.channels, cf.bits);
|
||||||
}
|
} else {
|
||||||
else
|
logf("audio stream %u: exact format %uHz/%uch/%ubit from the game's Initialize", slot, cf.rate, cf.channels,
|
||||||
{
|
cf.bits);
|
||||||
logf("audio stream %u: exact format %uHz/%uch/%ubit from the game's Initialize", slot, cf.rate,
|
|
||||||
cf.channels, cf.bits);
|
|
||||||
}
|
}
|
||||||
publish_stream_info_locked(slot, cf, state, 0);
|
publish_stream_info_locked(slot, cf, state, 0);
|
||||||
|
|
||||||
@@ -544,10 +496,8 @@ void register_render_client_locked(IAudioRenderClient* rc, const CapturedFormat&
|
|||||||
// True if `rc` already occupies a tracked debug slot (lock-free scan).
|
// True if `rc` already occupies a tracked debug slot (lock-free scan).
|
||||||
bool stream_tracked(IAudioRenderClient* rc)
|
bool stream_tracked(IAudioRenderClient* rc)
|
||||||
{
|
{
|
||||||
for (auto& s : g_streams)
|
for (auto& s : g_streams) {
|
||||||
{
|
if (s.client.load(std::memory_order_acquire) == rc) {
|
||||||
if (s.client.load(std::memory_order_acquire) == rc)
|
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,17 +509,14 @@ bool stream_tracked(IAudioRenderClient* rc)
|
|||||||
// as a best guess. Non-blocking: if setup is momentarily busy, retry next call.
|
// as a best guess. Non-blocking: if setup is momentarily busy, retry next call.
|
||||||
void try_register_lazy(IAudioRenderClient* rc)
|
void try_register_lazy(IAudioRenderClient* rc)
|
||||||
{
|
{
|
||||||
if (g_have_mix_format.load(std::memory_order_acquire) == 0)
|
if (g_have_mix_format.load(std::memory_order_acquire) == 0) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::unique_lock<std::mutex> lock(g_setup_mutex, std::try_to_lock);
|
std::unique_lock<std::mutex> lock(g_setup_mutex, std::try_to_lock);
|
||||||
if (!lock.owns_lock())
|
if (!lock.owns_lock()) {
|
||||||
{
|
|
||||||
return; // another thread is in setup; try again on the next buffer
|
return; // another thread is in setup; try again on the next buffer
|
||||||
}
|
}
|
||||||
if (stream_tracked(rc))
|
if (stream_tracked(rc)) {
|
||||||
{
|
|
||||||
return; // a concurrent path registered it first
|
return; // a concurrent path registered it first
|
||||||
}
|
}
|
||||||
logf("try_register_lazy: discovered pre-existing render client rc=%p", rc);
|
logf("try_register_lazy: discovered pre-existing render client rc=%p", rc);
|
||||||
@@ -581,12 +528,11 @@ HRESULT STDMETHODCALLTYPE hk_Initialize(IAudioClient* self, AUDCLNT_SHAREMODE mo
|
|||||||
const WAVEFORMATEX* format, LPCGUID session)
|
const WAVEFORMATEX* format, LPCGUID session)
|
||||||
{
|
{
|
||||||
hook_note_call(g_id_initialize);
|
hook_note_call(g_id_initialize);
|
||||||
const HRESULT hr = g_vh_initialize.original<InitializeFn>()(self, mode, flags, buffer_duration,
|
const HRESULT hr =
|
||||||
periodicity, format, session);
|
g_vh_initialize.original<InitializeFn>()(self, mode, flags, buffer_duration, periodicity, format, session);
|
||||||
logf("hk_Initialize: client=%p mode=%d flags=0x%lX hr=0x%08lX fmt=%s", self, mode,
|
logf("hk_Initialize: client=%p mode=%d flags=0x%lX hr=0x%08lX fmt=%s", self, mode,
|
||||||
static_cast<unsigned long>(flags), static_cast<unsigned long>(hr), format ? "yes" : "null");
|
static_cast<unsigned long>(flags), static_cast<unsigned long>(hr), format ? "yes" : "null");
|
||||||
if (SUCCEEDED(hr) && format != nullptr)
|
if (SUCCEEDED(hr) && format != nullptr) {
|
||||||
{
|
|
||||||
std::scoped_lock lock(g_setup_mutex);
|
std::scoped_lock lock(g_setup_mutex);
|
||||||
g_client_formats[self] = capture_format(format);
|
g_client_formats[self] = capture_format(format);
|
||||||
}
|
}
|
||||||
@@ -600,33 +546,28 @@ HRESULT STDMETHODCALLTYPE hk_GetService(IAudioClient* self, REFIID riid, void**
|
|||||||
const bool is_render = (riid == __uuidof(IAudioRenderClient));
|
const bool is_render = (riid == __uuidof(IAudioRenderClient));
|
||||||
logf("hk_GetService: client=%p hr=0x%08lX render_client=%d", self, static_cast<unsigned long>(hr),
|
logf("hk_GetService: client=%p hr=0x%08lX render_client=%d", self, static_cast<unsigned long>(hr),
|
||||||
is_render ? 1 : 0);
|
is_render ? 1 : 0);
|
||||||
if (SUCCEEDED(hr) && ppv != nullptr && *ppv != nullptr && riid == __uuidof(IAudioRenderClient))
|
if (SUCCEEDED(hr) && ppv != nullptr && *ppv != nullptr && riid == __uuidof(IAudioRenderClient)) {
|
||||||
{
|
|
||||||
CapturedFormat cf;
|
CapturedFormat cf;
|
||||||
bool have = false;
|
bool have = false;
|
||||||
{
|
{
|
||||||
std::scoped_lock lock(g_setup_mutex);
|
std::scoped_lock lock(g_setup_mutex);
|
||||||
auto it = g_client_formats.find(self);
|
auto it = g_client_formats.find(self);
|
||||||
if (it != g_client_formats.end())
|
if (it != g_client_formats.end()) {
|
||||||
{
|
|
||||||
cf = it->second;
|
cf = it->second;
|
||||||
have = true;
|
have = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback for IAudioClient3::InitializeSharedAudioStream (no Initialize
|
// Fallback for IAudioClient3::InitializeSharedAudioStream (no Initialize
|
||||||
// format): the shared-mode format is the device mix format.
|
// format): the shared-mode format is the device mix format.
|
||||||
if (!have)
|
if (!have) {
|
||||||
{
|
|
||||||
WAVEFORMATEX* mix = nullptr;
|
WAVEFORMATEX* mix = nullptr;
|
||||||
if (SUCCEEDED(self->GetMixFormat(&mix)) && mix != nullptr)
|
if (SUCCEEDED(self->GetMixFormat(&mix)) && mix != nullptr) {
|
||||||
{
|
|
||||||
cf = capture_format(mix);
|
cf = capture_format(mix);
|
||||||
have = true;
|
have = true;
|
||||||
CoTaskMemFree(mix);
|
CoTaskMemFree(mix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (have)
|
if (have) {
|
||||||
{
|
|
||||||
std::scoped_lock lock(g_setup_mutex);
|
std::scoped_lock lock(g_setup_mutex);
|
||||||
// We saw this client's Initialize (or its shared-mode mix format), so the rate
|
// We saw this client's Initialize (or its shared-mode mix format), so the rate
|
||||||
// is exact, not a guess.
|
// is exact, not a guess.
|
||||||
@@ -639,28 +580,25 @@ HRESULT STDMETHODCALLTYPE hk_GetService(IAudioClient* self, REFIID riid, void**
|
|||||||
void install_audioclient_hooks(IAudioClient* ac)
|
void install_audioclient_hooks(IAudioClient* ac)
|
||||||
{
|
{
|
||||||
std::scoped_lock lock(g_setup_mutex);
|
std::scoped_lock lock(g_setup_mutex);
|
||||||
if (g_audioclient_hooked)
|
if (g_audioclient_hooked) {
|
||||||
{
|
|
||||||
return; // shared vtable: hook the first IAudioClient we see, covers all
|
return; // shared vtable: hook the first IAudioClient we see, covers all
|
||||||
}
|
}
|
||||||
g_vh_initialize.install(ac, kIdx_IAudioClient_Initialize, reinterpret_cast<void*>(&hk_Initialize));
|
g_vh_initialize.install(ac, kIdx_IAudioClient_Initialize, reinterpret_cast<void*>(&hk_Initialize));
|
||||||
g_vh_getservice.install(ac, kIdx_IAudioClient_GetService, reinterpret_cast<void*>(&hk_GetService));
|
g_vh_getservice.install(ac, kIdx_IAudioClient_GetService, reinterpret_cast<void*>(&hk_GetService));
|
||||||
g_audioclient_hooked = (static_cast<bool>(g_vh_initialize) && static_cast<bool>(g_vh_getservice));
|
g_audioclient_hooked = (static_cast<bool>(g_vh_initialize) && static_cast<bool>(g_vh_getservice));
|
||||||
logf("install_audioclient_hooks: ac=%p initialize=%d getservice=%d", ac,
|
logf("install_audioclient_hooks: ac=%p initialize=%d getservice=%d", ac, static_cast<bool>(g_vh_initialize) ? 1 : 0,
|
||||||
static_cast<bool>(g_vh_initialize) ? 1 : 0, static_cast<bool>(g_vh_getservice) ? 1 : 0);
|
static_cast<bool>(g_vh_getservice) ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE hk_Activate(IMMDevice* self, REFIID riid, DWORD cls_ctx, PROPVARIANT* params,
|
HRESULT STDMETHODCALLTYPE hk_Activate(IMMDevice* self, REFIID riid, DWORD cls_ctx, PROPVARIANT* params, void** ppv)
|
||||||
void** ppv)
|
|
||||||
{
|
{
|
||||||
hook_note_call(g_id_activate);
|
hook_note_call(g_id_activate);
|
||||||
const HRESULT hr = g_vh_activate.original<ActivateFn>()(self, riid, cls_ctx, params, ppv);
|
const HRESULT hr = g_vh_activate.original<ActivateFn>()(self, riid, cls_ctx, params, ppv);
|
||||||
const bool is_audioclient = (riid == __uuidof(IAudioClient) || riid == __uuidof(IAudioClient2) ||
|
const bool is_audioclient =
|
||||||
riid == __uuidof(IAudioClient3));
|
(riid == __uuidof(IAudioClient) || riid == __uuidof(IAudioClient2) || riid == __uuidof(IAudioClient3));
|
||||||
logf("hk_Activate: device=%p hr=0x%08lX audioclient=%d", self, static_cast<unsigned long>(hr),
|
logf("hk_Activate: device=%p hr=0x%08lX audioclient=%d", self, static_cast<unsigned long>(hr),
|
||||||
is_audioclient ? 1 : 0);
|
is_audioclient ? 1 : 0);
|
||||||
if (SUCCEEDED(hr) && ppv != nullptr && *ppv != nullptr && is_audioclient)
|
if (SUCCEEDED(hr) && ppv != nullptr && *ppv != nullptr && is_audioclient) {
|
||||||
{
|
|
||||||
install_audioclient_hooks(static_cast<IAudioClient*>(*ppv));
|
install_audioclient_hooks(static_cast<IAudioClient*>(*ppv));
|
||||||
}
|
}
|
||||||
return hr;
|
return hr;
|
||||||
@@ -668,62 +606,51 @@ HRESULT STDMETHODCALLTYPE hk_Activate(IMMDevice* self, REFIID riid, DWORD cls_ct
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
// Build the probe COM objects (enumerator -> device -> client -> render) and capture the
|
// Build the probe COM objects (enumerator -> device -> client -> render) and capture the
|
||||||
// device mix format. Created ONCE and kept for the DLL's lifetime: every instance of a
|
// device mix format. Created ONCE and kept for the DLL's lifetime: every instance of a
|
||||||
// coclass shares one vtable, so a toggle then only re-swaps vtable slots on these kept
|
// coclass shares one vtable, so a toggle then only re-swaps vtable slots on these kept
|
||||||
// objects -- no COM create/destroy churn (which races AudioSes). Caller holds g_setup_mutex.
|
// objects -- no COM create/destroy churn (which races AudioSes). Caller holds g_setup_mutex.
|
||||||
bool build_probe_locked()
|
bool build_probe_locked()
|
||||||
{
|
{
|
||||||
if (g_self_device != nullptr)
|
if (g_self_device != nullptr) {
|
||||||
{
|
|
||||||
return true; // already built
|
return true; // already built
|
||||||
}
|
}
|
||||||
IMMDeviceEnumerator* enumerator = nullptr;
|
IMMDeviceEnumerator* enumerator = nullptr;
|
||||||
if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
|
if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator),
|
||||||
__uuidof(IMMDeviceEnumerator), reinterpret_cast<void**>(&enumerator))))
|
reinterpret_cast<void**>(&enumerator)))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
IMMDevice* device = nullptr;
|
IMMDevice* device = nullptr;
|
||||||
const HRESULT hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device);
|
const HRESULT hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device);
|
||||||
enumerator->Release(); // only needed to reach the device
|
enumerator->Release(); // only needed to reach the device
|
||||||
if (FAILED(hr) || device == nullptr)
|
if (FAILED(hr) || device == nullptr) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
g_self_device = device; // kept alive (Activate hook re-installs from its vtable)
|
g_self_device = device; // kept alive (Activate hook re-installs from its vtable)
|
||||||
|
|
||||||
IAudioRenderClient* self_render = nullptr;
|
IAudioRenderClient* self_render = nullptr;
|
||||||
HRESULT ah = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr,
|
HRESULT ah =
|
||||||
reinterpret_cast<void**>(&g_self_client));
|
device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast<void**>(&g_self_client));
|
||||||
if (SUCCEEDED(ah) && g_self_client != nullptr)
|
if (SUCCEEDED(ah) && g_self_client != nullptr) {
|
||||||
{
|
|
||||||
WAVEFORMATEX* mix = nullptr;
|
WAVEFORMATEX* mix = nullptr;
|
||||||
if (SUCCEEDED(g_self_client->GetMixFormat(&mix)) && mix != nullptr)
|
if (SUCCEEDED(g_self_client->GetMixFormat(&mix)) && mix != nullptr) {
|
||||||
{
|
|
||||||
g_mix_format = capture_format(mix);
|
g_mix_format = capture_format(mix);
|
||||||
g_have_mix_format.store(1, std::memory_order_release);
|
g_have_mix_format.store(1, std::memory_order_release);
|
||||||
constexpr REFERENCE_TIME kBuf = 10 * 10000; // 10 ms; never started
|
constexpr REFERENCE_TIME kBuf = 10 * 10000; // 10 ms; never started
|
||||||
HRESULT ih = g_self_client->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, kBuf, 0, mix, nullptr);
|
HRESULT ih = g_self_client->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, kBuf, 0, mix, nullptr);
|
||||||
if (SUCCEEDED(ih))
|
if (SUCCEEDED(ih)) {
|
||||||
{
|
ih = g_self_client->GetService(__uuidof(IAudioRenderClient), reinterpret_cast<void**>(&self_render));
|
||||||
ih = g_self_client->GetService(__uuidof(IAudioRenderClient),
|
|
||||||
reinterpret_cast<void**>(&self_render));
|
|
||||||
}
|
}
|
||||||
logf("build_probe: client init=0x%08lX render=%p mix=%uHz/%uch/%ubit tag=%u",
|
logf("build_probe: client init=0x%08lX render=%p mix=%uHz/%uch/%ubit tag=%u",
|
||||||
static_cast<unsigned long>(ih), self_render, g_mix_format.rate, g_mix_format.channels,
|
static_cast<unsigned long>(ih), self_render, g_mix_format.rate, g_mix_format.channels,
|
||||||
g_mix_format.bits, g_mix_format.tag);
|
g_mix_format.bits, g_mix_format.tag);
|
||||||
CoTaskMemFree(mix);
|
CoTaskMemFree(mix);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
logf("build_probe: Activate(IAudioClient) failed hr=0x%08lX", static_cast<unsigned long>(ah));
|
logf("build_probe: Activate(IAudioClient) failed hr=0x%08lX", static_cast<unsigned long>(ah));
|
||||||
}
|
}
|
||||||
if (self_render != nullptr)
|
if (self_render != nullptr) {
|
||||||
{
|
|
||||||
g_self_render.store(self_render, std::memory_order_release);
|
g_self_render.store(self_render, std::memory_order_release);
|
||||||
}
|
}
|
||||||
return true; // device built; render may be null on odd setups (Activate hook still works)
|
return true; // device built; render may be null on odd setups (Activate hook still works)
|
||||||
@@ -733,18 +660,14 @@ bool build_probe_locked()
|
|||||||
// g_setup_mutex.
|
// g_setup_mutex.
|
||||||
void install_detours_locked()
|
void install_detours_locked()
|
||||||
{
|
{
|
||||||
if (g_self_device == nullptr)
|
if (g_self_device == nullptr) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
g_hook_epoch.fetch_add(1, std::memory_order_release); // new epoch: invalidate any straddling GetBuffer
|
g_hook_epoch.fetch_add(1, std::memory_order_release); // new epoch: invalidate any straddling GetBuffer
|
||||||
g_vh_activate.install(g_self_device, kIdx_IMMDevice_Activate, reinterpret_cast<void*>(&hk_Activate));
|
g_vh_activate.install(g_self_device, kIdx_IMMDevice_Activate, reinterpret_cast<void*>(&hk_Activate));
|
||||||
if (IAudioRenderClient* sr = g_self_render.load(std::memory_order_acquire))
|
if (IAudioRenderClient* sr = g_self_render.load(std::memory_order_acquire)) {
|
||||||
{
|
g_vh_initialize.install(g_self_client, kIdx_IAudioClient_Initialize, reinterpret_cast<void*>(&hk_Initialize));
|
||||||
g_vh_initialize.install(g_self_client, kIdx_IAudioClient_Initialize,
|
g_vh_getservice.install(g_self_client, kIdx_IAudioClient_GetService, reinterpret_cast<void*>(&hk_GetService));
|
||||||
reinterpret_cast<void*>(&hk_Initialize));
|
|
||||||
g_vh_getservice.install(g_self_client, kIdx_IAudioClient_GetService,
|
|
||||||
reinterpret_cast<void*>(&hk_GetService));
|
|
||||||
g_vh_getbuffer.install(sr, kIdx_IAudioRenderClient_GetBuffer, reinterpret_cast<void*>(&hk_GetBuffer));
|
g_vh_getbuffer.install(sr, kIdx_IAudioRenderClient_GetBuffer, reinterpret_cast<void*>(&hk_GetBuffer));
|
||||||
g_vh_releasebuffer.install(sr, kIdx_IAudioRenderClient_ReleaseBuffer,
|
g_vh_releasebuffer.install(sr, kIdx_IAudioRenderClient_ReleaseBuffer,
|
||||||
reinterpret_cast<void*>(&hk_ReleaseBuffer));
|
reinterpret_cast<void*>(&hk_ReleaseBuffer));
|
||||||
@@ -755,10 +678,9 @@ void install_detours_locked()
|
|||||||
hook_set_installed(g_id_getservice, static_cast<bool>(g_vh_getservice));
|
hook_set_installed(g_id_getservice, static_cast<bool>(g_vh_getservice));
|
||||||
hook_set_installed(g_id_getbuffer, static_cast<bool>(g_vh_getbuffer));
|
hook_set_installed(g_id_getbuffer, static_cast<bool>(g_vh_getbuffer));
|
||||||
hook_set_installed(g_id_releasebuffer, static_cast<bool>(g_vh_releasebuffer));
|
hook_set_installed(g_id_releasebuffer, static_cast<bool>(g_vh_releasebuffer));
|
||||||
logf("install_detours: activate=%d init=%d getsvc=%d getbuf=%d relbuf=%d",
|
logf("install_detours: activate=%d init=%d getsvc=%d getbuf=%d relbuf=%d", static_cast<bool>(g_vh_activate) ? 1 : 0,
|
||||||
static_cast<bool>(g_vh_activate) ? 1 : 0, static_cast<bool>(g_vh_initialize) ? 1 : 0,
|
static_cast<bool>(g_vh_initialize) ? 1 : 0, static_cast<bool>(g_vh_getservice) ? 1 : 0,
|
||||||
static_cast<bool>(g_vh_getservice) ? 1 : 0, static_cast<bool>(g_vh_getbuffer) ? 1 : 0,
|
static_cast<bool>(g_vh_getbuffer) ? 1 : 0, static_cast<bool>(g_vh_releasebuffer) ? 1 : 0);
|
||||||
static_cast<bool>(g_vh_releasebuffer) ? 1 : 0);
|
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -767,8 +689,7 @@ bool install_audio_hooks(IpcClient& ipc, AudioRingHeader* ring)
|
|||||||
std::scoped_lock lock(g_setup_mutex);
|
std::scoped_lock lock(g_setup_mutex);
|
||||||
g_ipc.store(&ipc, std::memory_order_release);
|
g_ipc.store(&ipc, std::memory_order_release);
|
||||||
g_rings[0].store(ring, std::memory_order_release);
|
g_rings[0].store(ring, std::memory_order_release);
|
||||||
if (g_vh_activate)
|
if (g_vh_activate) {
|
||||||
{
|
|
||||||
return true; // detours already installed
|
return true; // detours already installed
|
||||||
}
|
}
|
||||||
if (g_id_activate < 0) // register the hook-list ids once
|
if (g_id_activate < 0) // register the hook-list ids once
|
||||||
@@ -790,17 +711,14 @@ bool install_audio_hooks(IpcClient& ipc, AudioRingHeader* ring)
|
|||||||
void republish_audio_format()
|
void republish_audio_format()
|
||||||
{
|
{
|
||||||
std::scoped_lock lock(g_setup_mutex);
|
std::scoped_lock lock(g_setup_mutex);
|
||||||
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i)
|
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i) {
|
||||||
{
|
|
||||||
AudioRingHeader* ring = g_rings[i].load(std::memory_order_acquire);
|
AudioRingHeader* ring = g_rings[i].load(std::memory_order_acquire);
|
||||||
if (ring == nullptr)
|
if (ring == nullptr) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Apply any operator command (re-measure / override) the host posted on this ring.
|
// Apply any operator command (re-measure / override) the host posted on this ring.
|
||||||
AudioRingOpCmd cmd;
|
AudioRingOpCmd cmd;
|
||||||
if (audio_ring_poll_op(*ring, g_last_op_seq[i], cmd) != AudioRingOp_None)
|
if (audio_ring_poll_op(*ring, g_last_op_seq[i], cmd) != AudioRingOp_None) {
|
||||||
{
|
|
||||||
apply_audio_op_locked(i, cmd);
|
apply_audio_op_locked(i, cmd);
|
||||||
}
|
}
|
||||||
// Publishes an exact format immediately; a guessed rate is measured first and
|
// Publishes an exact format immediately; a guessed rate is measured first and
|
||||||
@@ -811,15 +729,13 @@ void republish_audio_format()
|
|||||||
|
|
||||||
void set_audio_ring(unsigned index, AudioRingHeader* ring)
|
void set_audio_ring(unsigned index, AudioRingHeader* ring)
|
||||||
{
|
{
|
||||||
if (index >= kMaxAudioStreams)
|
if (index >= kMaxAudioStreams) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The worker thread re-attaches every tick (idempotent); only log when the ring
|
// The worker thread re-attaches every tick (idempotent); only log when the ring
|
||||||
// pointer actually changes so the log isn't flooded with identical lines.
|
// pointer actually changes so the log isn't flooded with identical lines.
|
||||||
AudioRingHeader* const prev = g_rings[index].exchange(ring, std::memory_order_acq_rel);
|
AudioRingHeader* const prev = g_rings[index].exchange(ring, std::memory_order_acq_rel);
|
||||||
if (prev != ring)
|
if (prev != ring) {
|
||||||
{
|
|
||||||
logf("set_audio_ring: index=%u ring=%p capture_enabled=%u", index, ring,
|
logf("set_audio_ring: index=%u ring=%p capture_enabled=%u", index, ring,
|
||||||
ring ? ring->capture_enabled.load(std::memory_order_relaxed) : 0u);
|
ring ? ring->capture_enabled.load(std::memory_order_relaxed) : 0u);
|
||||||
}
|
}
|
||||||
@@ -836,7 +752,8 @@ void remove_audio_hooks()
|
|||||||
// objects alive -- a re-enable just re-swaps the slots, no COM churn. The probe is only
|
// objects alive -- a re-enable just re-swaps the slots, no COM churn. The probe is only
|
||||||
// released on detach (shutdown_audio_hooks). m_original stays valid (see VtableHook), so
|
// released on detach (shutdown_audio_hooks). m_original stays valid (see VtableHook), so
|
||||||
// an in-flight detour on the audio thread completes safely after the restore.
|
// an in-flight detour on the audio thread completes safely after the restore.
|
||||||
g_hook_epoch.fetch_add(1, std::memory_order_release); // new epoch: a later capture won't trust a pre-toggle GetBuffer
|
g_hook_epoch.fetch_add(1,
|
||||||
|
std::memory_order_release); // new epoch: a later capture won't trust a pre-toggle GetBuffer
|
||||||
g_vh_releasebuffer.remove();
|
g_vh_releasebuffer.remove();
|
||||||
g_vh_getbuffer.remove();
|
g_vh_getbuffer.remove();
|
||||||
g_vh_getservice.remove();
|
g_vh_getservice.remove();
|
||||||
@@ -858,8 +775,7 @@ void remove_audio_hooks()
|
|||||||
g_streams_seen.store(0, std::memory_order_relaxed);
|
g_streams_seen.store(0, std::memory_order_relaxed);
|
||||||
g_frames_captured.store(0, std::memory_order_relaxed);
|
g_frames_captured.store(0, std::memory_order_relaxed);
|
||||||
g_frames_silenced.store(0, std::memory_order_relaxed);
|
g_frames_silenced.store(0, std::memory_order_relaxed);
|
||||||
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i)
|
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i) {
|
||||||
{
|
|
||||||
g_streams[i].client.store(nullptr, std::memory_order_relaxed);
|
g_streams[i].client.store(nullptr, std::memory_order_relaxed);
|
||||||
g_streams[i].frames.store(0, std::memory_order_relaxed);
|
g_streams[i].frames.store(0, std::memory_order_relaxed);
|
||||||
g_streams[i].block_align.store(0, std::memory_order_relaxed);
|
g_streams[i].block_align.store(0, std::memory_order_relaxed);
|
||||||
@@ -880,17 +796,14 @@ void shutdown_audio_hooks()
|
|||||||
remove_audio_hooks(); // restore vtables + clear state (takes the lock)
|
remove_audio_hooks(); // restore vtables + clear state (takes the lock)
|
||||||
// Now safe to release the kept probe objects (called only on DLL detach).
|
// Now safe to release the kept probe objects (called only on DLL detach).
|
||||||
std::scoped_lock lock(g_setup_mutex);
|
std::scoped_lock lock(g_setup_mutex);
|
||||||
if (IAudioRenderClient* sr = g_self_render.exchange(nullptr, std::memory_order_acq_rel))
|
if (IAudioRenderClient* sr = g_self_render.exchange(nullptr, std::memory_order_acq_rel)) {
|
||||||
{
|
|
||||||
sr->Release();
|
sr->Release();
|
||||||
}
|
}
|
||||||
if (g_self_client != nullptr)
|
if (g_self_client != nullptr) {
|
||||||
{
|
|
||||||
g_self_client->Release();
|
g_self_client->Release();
|
||||||
g_self_client = nullptr;
|
g_self_client = nullptr;
|
||||||
}
|
}
|
||||||
if (g_self_device != nullptr)
|
if (g_self_device != nullptr) {
|
||||||
{
|
|
||||||
g_self_device->Release();
|
g_self_device->Release();
|
||||||
g_self_device = nullptr;
|
g_self_device = nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,7 @@
|
|||||||
#include "coop/audio_ring.hpp"
|
#include "coop/audio_ring.hpp"
|
||||||
#include "ipc_client.hpp"
|
#include "ipc_client.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
// Installs the render-path hooks. `ipc` must outlive the hooks (used for the
|
// Installs the render-path hooks. `ipc` must outlive the hooks (used for the
|
||||||
// stream-count diagnostics in HookStatus). `ring` may be null — counting still
|
// stream-count diagnostics in HookStatus). `ring` may be null — counting still
|
||||||
|
|||||||
@@ -19,11 +19,9 @@
|
|||||||
#include "shared_video_texture.hpp"
|
#include "shared_video_texture.hpp"
|
||||||
#include "vtable_hook.hpp"
|
#include "vtable_hook.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
DetourGate g_gate; // drains in-flight Present detours before remove frees the shared D3D state
|
DetourGate g_gate; // drains in-flight Present detours before remove frees the shared D3D state
|
||||||
|
|
||||||
@@ -65,14 +63,12 @@ thread_local bool t_in_present = false;
|
|||||||
|
|
||||||
bool ensure_device()
|
bool ensure_device()
|
||||||
{
|
{
|
||||||
if (g_device != nullptr)
|
if (g_device != nullptr) {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0,
|
const HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION,
|
||||||
D3D11_SDK_VERSION, &g_device, nullptr, &g_ctx);
|
&g_device, nullptr, &g_ctx);
|
||||||
if (FAILED(hr) || g_device == nullptr)
|
if (FAILED(hr) || g_device == nullptr) {
|
||||||
{
|
|
||||||
logf("d3d9: D3D11CreateDevice failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
logf("d3d9: D3D11CreateDevice failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -81,13 +77,11 @@ bool ensure_device()
|
|||||||
|
|
||||||
void release_sysmem()
|
void release_sysmem()
|
||||||
{
|
{
|
||||||
if (g_sysmem != nullptr)
|
if (g_sysmem != nullptr) {
|
||||||
{
|
|
||||||
g_sysmem->Release();
|
g_sysmem->Release();
|
||||||
g_sysmem = nullptr;
|
g_sysmem = nullptr;
|
||||||
}
|
}
|
||||||
if (g_sysmem_dev != nullptr)
|
if (g_sysmem_dev != nullptr) {
|
||||||
{
|
|
||||||
g_sysmem_dev->Release();
|
g_sysmem_dev->Release();
|
||||||
g_sysmem_dev = nullptr;
|
g_sysmem_dev = nullptr;
|
||||||
}
|
}
|
||||||
@@ -99,8 +93,7 @@ void release_sysmem()
|
|||||||
void capture_d3d9(IDirect3DDevice9* dev)
|
void capture_d3d9(IDirect3DDevice9* dev)
|
||||||
{
|
{
|
||||||
IDirect3DSurface9* back = nullptr;
|
IDirect3DSurface9* back = nullptr;
|
||||||
if (FAILED(dev->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &back)) || back == nullptr)
|
if (FAILED(dev->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &back)) || back == nullptr) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
D3DSURFACE_DESC d{};
|
D3DSURFACE_DESC d{};
|
||||||
@@ -108,10 +101,8 @@ void capture_d3d9(IDirect3DDevice9* dev)
|
|||||||
const UINT w = d.Width;
|
const UINT w = d.Width;
|
||||||
const UINT h = d.Height;
|
const UINT h = d.Height;
|
||||||
// We only handle the standard 32-bit BGRX/BGRA back buffers (the common D3D9 case).
|
// We only handle the standard 32-bit BGRX/BGRA back buffers (the common D3D9 case).
|
||||||
if ((d.Format != D3DFMT_X8R8G8B8 && d.Format != D3DFMT_A8R8G8B8) || w == 0 || h == 0)
|
if ((d.Format != D3DFMT_X8R8G8B8 && d.Format != D3DFMT_A8R8G8B8) || w == 0 || h == 0) {
|
||||||
{
|
if (!g_unsupported_logged) {
|
||||||
if (!g_unsupported_logged)
|
|
||||||
{
|
|
||||||
logf("d3d9: unsupported backbuffer format=%d (only X8R8G8B8 / A8R8G8B8); idle", static_cast<int>(d.Format));
|
logf("d3d9: unsupported backbuffer format=%d (only X8R8G8B8 / A8R8G8B8); idle", static_cast<int>(d.Format));
|
||||||
g_unsupported_logged = true;
|
g_unsupported_logged = true;
|
||||||
}
|
}
|
||||||
@@ -120,12 +111,11 @@ void capture_d3d9(IDirect3DDevice9* dev)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// (Re)create the system-memory read-back surface on the game's device.
|
// (Re)create the system-memory read-back surface on the game's device.
|
||||||
if (!(g_sysmem != nullptr && g_sysmem_dev == dev && g_sysmem_w == w && g_sysmem_h == h && g_sysmem_fmt == d.Format))
|
if (!(g_sysmem != nullptr && g_sysmem_dev == dev && g_sysmem_w == w && g_sysmem_h == h
|
||||||
{
|
&& g_sysmem_fmt == d.Format)) {
|
||||||
release_sysmem();
|
release_sysmem();
|
||||||
if (SUCCEEDED(dev->CreateOffscreenPlainSurface(w, h, d.Format, D3DPOOL_SYSTEMMEM, &g_sysmem, nullptr)) &&
|
if (SUCCEEDED(dev->CreateOffscreenPlainSurface(w, h, d.Format, D3DPOOL_SYSTEMMEM, &g_sysmem, nullptr))
|
||||||
g_sysmem != nullptr)
|
&& g_sysmem != nullptr) {
|
||||||
{
|
|
||||||
g_sysmem_dev = dev;
|
g_sysmem_dev = dev;
|
||||||
dev->AddRef();
|
dev->AddRef();
|
||||||
g_sysmem_w = w;
|
g_sysmem_w = w;
|
||||||
@@ -138,21 +128,18 @@ void capture_d3d9(IDirect3DDevice9* dev)
|
|||||||
if (g_sysmem != nullptr && SUCCEEDED(dev->GetRenderTargetData(back, g_sysmem))) // GPU->sysmem, blocks
|
if (g_sysmem != nullptr && SUCCEEDED(dev->GetRenderTargetData(back, g_sysmem))) // GPU->sysmem, blocks
|
||||||
{
|
{
|
||||||
D3DLOCKED_RECT lr{};
|
D3DLOCKED_RECT lr{};
|
||||||
if (SUCCEEDED(g_sysmem->LockRect(&lr, nullptr, D3DLOCK_READONLY)) && lr.pBits != nullptr)
|
if (SUCCEEDED(g_sysmem->LockRect(&lr, nullptr, D3DLOCK_READONLY)) && lr.pBits != nullptr) {
|
||||||
{
|
|
||||||
const size_t dst_row = static_cast<size_t>(w) * 4;
|
const size_t dst_row = static_cast<size_t>(w) * 4;
|
||||||
if (g_rgba.size() != dst_row * h)
|
if (g_rgba.size() != dst_row * h) {
|
||||||
{
|
|
||||||
g_rgba.resize(dst_row * h);
|
g_rgba.resize(dst_row * h);
|
||||||
}
|
}
|
||||||
// X8R8G8B8 / A8R8G8B8 store as little-endian 0xAARRGGBB -> bytes B,G,R,A. Swizzle to
|
// X8R8G8B8 / A8R8G8B8 store as little-endian 0xAARRGGBB -> bytes B,G,R,A. Swizzle to
|
||||||
// R,G,B,A and force opaque alpha so the host's RGBA decode matches the other backends.
|
// R,G,B,A and force opaque alpha so the host's RGBA decode matches the other backends.
|
||||||
for (UINT y = 0; y < h; ++y)
|
for (UINT y = 0; y < h; ++y) {
|
||||||
{
|
const unsigned char* src =
|
||||||
const unsigned char* src = static_cast<const unsigned char*>(lr.pBits) + static_cast<size_t>(y) * lr.Pitch;
|
static_cast<const unsigned char*>(lr.pBits) + static_cast<size_t>(y) * lr.Pitch;
|
||||||
unsigned char* out = g_rgba.data() + static_cast<size_t>(y) * dst_row;
|
unsigned char* out = g_rgba.data() + static_cast<size_t>(y) * dst_row;
|
||||||
for (UINT x = 0; x < w; ++x)
|
for (UINT x = 0; x < w; ++x) {
|
||||||
{
|
|
||||||
out[x * 4 + 0] = src[x * 4 + 2]; // R
|
out[x * 4 + 0] = src[x * 4 + 2]; // R
|
||||||
out[x * 4 + 1] = src[x * 4 + 1]; // G
|
out[x * 4 + 1] = src[x * 4 + 1]; // G
|
||||||
out[x * 4 + 2] = src[x * 4 + 0]; // B
|
out[x * 4 + 2] = src[x * 4 + 0]; // B
|
||||||
@@ -162,10 +149,8 @@ void capture_d3d9(IDirect3DDevice9* dev)
|
|||||||
g_sysmem->UnlockRect();
|
g_sysmem->UnlockRect();
|
||||||
|
|
||||||
// DXGI_FORMAT_R8G8B8A8_UNORM: we swizzle the D3D9 BGRA backbuffer to RGBA above.
|
// DXGI_FORMAT_R8G8B8A8_UNORM: we swizzle the D3D9 BGRA backbuffer to RGBA above.
|
||||||
if (ensure_device() &&
|
if (ensure_device() && g_shared.ensure(g_device, w, h, DXGI_FORMAT_R8G8B8A8_UNORM, g_pid, "d3d9")
|
||||||
g_shared.ensure(g_device, w, h, DXGI_FORMAT_R8G8B8A8_UNORM, g_pid, "d3d9") &&
|
&& g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK) {
|
||||||
g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK)
|
|
||||||
{
|
|
||||||
g_ctx->UpdateSubresource(g_shared.texture(), 0, nullptr, g_rgba.data(), static_cast<UINT>(dst_row), 0);
|
g_ctx->UpdateSubresource(g_shared.texture(), 0, nullptr, g_rgba.data(), static_cast<UINT>(dst_row), 0);
|
||||||
g_ctx->Flush();
|
g_ctx->Flush();
|
||||||
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
|
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
|
||||||
@@ -174,11 +159,9 @@ void capture_d3d9(IDirect3DDevice9* dev)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shared)
|
if (shared) {
|
||||||
{
|
|
||||||
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
|
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->publish_video_frame(w, h, static_cast<std::uint32_t>(DXGI_FORMAT_R8G8B8A8_UNORM));
|
g_ipc->publish_video_frame(w, h, static_cast<std::uint32_t>(DXGI_FORMAT_R8G8B8A8_UNORM));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,12 +174,10 @@ HRESULT STDMETHODCALLTYPE hk_Present9(IDirect3DDevice9* dev, const RECT* src, co
|
|||||||
DetourGate::Guard guard(g_gate); // keep the shared D3D state alive for this whole detour
|
DetourGate::Guard guard(g_gate); // keep the shared D3D state alive for this whole detour
|
||||||
hook_note_call(g_id_present9);
|
hook_note_call(g_id_present9);
|
||||||
g_presents.fetch_add(1, std::memory_order_relaxed);
|
g_presents.fetch_add(1, std::memory_order_relaxed);
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->note_present();
|
g_ipc->note_present();
|
||||||
}
|
}
|
||||||
if (!t_in_present)
|
if (!t_in_present) {
|
||||||
{
|
|
||||||
t_in_present = true;
|
t_in_present = true;
|
||||||
capture_d3d9(dev);
|
capture_d3d9(dev);
|
||||||
t_in_present = false;
|
t_in_present = false;
|
||||||
@@ -213,19 +194,16 @@ HRESULT STDMETHODCALLTYPE hk_Present9(IDirect3DDevice9* dev, const RECT* src, co
|
|||||||
void* grab_present9_address()
|
void* grab_present9_address()
|
||||||
{
|
{
|
||||||
HMODULE d3d9 = GetModuleHandleW(L"d3d9.dll");
|
HMODULE d3d9 = GetModuleHandleW(L"d3d9.dll");
|
||||||
if (d3d9 == nullptr)
|
if (d3d9 == nullptr) {
|
||||||
{
|
|
||||||
return nullptr; // not a D3D9 game
|
return nullptr; // not a D3D9 game
|
||||||
}
|
}
|
||||||
using PFN_Direct3DCreate9 = IDirect3D9*(WINAPI*)(UINT);
|
using PFN_Direct3DCreate9 = IDirect3D9*(WINAPI*)(UINT);
|
||||||
auto create = reinterpret_cast<PFN_Direct3DCreate9>(GetProcAddress(d3d9, "Direct3DCreate9"));
|
auto create = reinterpret_cast<PFN_Direct3DCreate9>(GetProcAddress(d3d9, "Direct3DCreate9"));
|
||||||
if (create == nullptr)
|
if (create == nullptr) {
|
||||||
{
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
IDirect3D9* d3d = create(D3D_SDK_VERSION);
|
IDirect3D9* d3d = create(D3D_SDK_VERSION);
|
||||||
if (d3d == nullptr)
|
if (d3d == nullptr) {
|
||||||
{
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,8 +217,7 @@ void* grab_present9_address()
|
|||||||
wc.hInstance, nullptr);
|
wc.hInstance, nullptr);
|
||||||
|
|
||||||
void* present = nullptr;
|
void* present = nullptr;
|
||||||
if (hwnd != nullptr)
|
if (hwnd != nullptr) {
|
||||||
{
|
|
||||||
D3DPRESENT_PARAMETERS pp{};
|
D3DPRESENT_PARAMETERS pp{};
|
||||||
pp.BackBufferWidth = 8;
|
pp.BackBufferWidth = 8;
|
||||||
pp.BackBufferHeight = 8;
|
pp.BackBufferHeight = 8;
|
||||||
@@ -251,9 +228,8 @@ void* grab_present9_address()
|
|||||||
pp.Windowed = TRUE;
|
pp.Windowed = TRUE;
|
||||||
IDirect3DDevice9* dev = nullptr;
|
IDirect3DDevice9* dev = nullptr;
|
||||||
if (SUCCEEDED(d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd,
|
if (SUCCEEDED(d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd,
|
||||||
D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED, &pp, &dev)) &&
|
D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED, &pp, &dev))
|
||||||
dev != nullptr)
|
&& dev != nullptr) {
|
||||||
{
|
|
||||||
present = vtable_method(dev, kIdx_IDirect3DDevice9_Present);
|
present = vtable_method(dev, kIdx_IDirect3DDevice9_Present);
|
||||||
dev->Release();
|
dev->Release();
|
||||||
}
|
}
|
||||||
@@ -270,16 +246,14 @@ bool install_d3d9_hooks(IpcClient& ipc)
|
|||||||
{
|
{
|
||||||
g_ipc = &ipc;
|
g_ipc = &ipc;
|
||||||
g_pid = GetCurrentProcessId();
|
g_pid = GetCurrentProcessId();
|
||||||
if (g_hk_present9.enabled())
|
if (g_hk_present9.enabled()) {
|
||||||
{
|
|
||||||
return true; // already installed (persistent hook; re-install below re-enables it)
|
return true; // already installed (persistent hook; re-install below re-enables it)
|
||||||
}
|
}
|
||||||
g_id_present9 = hook_register("IDirect3DDevice9::Present", HookSubsys_Video);
|
g_id_present9 = hook_register("IDirect3DDevice9::Present", HookSubsys_Video);
|
||||||
g_unsupported_logged = false;
|
g_unsupported_logged = false;
|
||||||
|
|
||||||
void* present = grab_present9_address();
|
void* present = grab_present9_address();
|
||||||
if (present == nullptr)
|
if (present == nullptr) {
|
||||||
{
|
|
||||||
hook_set_installed(g_id_present9, false); // not a D3D9 game (or no probe device)
|
hook_set_installed(g_id_present9, false); // not a D3D9 game (or no probe device)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -301,13 +275,11 @@ void remove_d3d9_hooks()
|
|||||||
g_gate.drain();
|
g_gate.drain();
|
||||||
g_shared.release();
|
g_shared.release();
|
||||||
release_sysmem();
|
release_sysmem();
|
||||||
if (g_ctx != nullptr)
|
if (g_ctx != nullptr) {
|
||||||
{
|
|
||||||
g_ctx->Release();
|
g_ctx->Release();
|
||||||
g_ctx = nullptr;
|
g_ctx = nullptr;
|
||||||
}
|
}
|
||||||
if (g_device != nullptr)
|
if (g_device != nullptr) {
|
||||||
{
|
|
||||||
g_device->Release();
|
g_device->Release();
|
||||||
g_device = nullptr;
|
g_device = nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,7 @@
|
|||||||
|
|
||||||
#include "ipc_client.hpp"
|
#include "ipc_client.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
// Installs the D3D9 Present hook. `ipc` must outlive the hook. Returns true if Present was
|
// Installs the D3D9 Present hook. `ipc` must outlive the hook. Returns true if Present was
|
||||||
// hooked (i.e. d3d9.dll is present and a probe device came up). Safe to call repeatedly.
|
// hooked (i.e. d3d9.dll is present and a probe device came up). Safe to call repeatedly.
|
||||||
|
|||||||
@@ -10,11 +10,9 @@
|
|||||||
|
|
||||||
#include "coop/log_ring.hpp"
|
#include "coop/log_ring.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
std::mutex g_log_mutex;
|
std::mutex g_log_mutex;
|
||||||
FILE* g_log_file = nullptr;
|
FILE* g_log_file = nullptr;
|
||||||
@@ -29,14 +27,12 @@ std::atomic<coop::LogRing*> g_log_ring{nullptr};
|
|||||||
bool logging_enabled()
|
bool logging_enabled()
|
||||||
{
|
{
|
||||||
wchar_t buf[8] = {};
|
wchar_t buf[8] = {};
|
||||||
if (GetEnvironmentVariableW(L"COOP_HOOK_LOG", buf, 8) > 0)
|
if (GetEnvironmentVariableW(L"COOP_HOOK_LOG", buf, 8) > 0) {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
wchar_t dir[MAX_PATH] = {};
|
wchar_t dir[MAX_PATH] = {};
|
||||||
const DWORD n = GetTempPathW(MAX_PATH, dir);
|
const DWORD n = GetTempPathW(MAX_PATH, dir);
|
||||||
if (n != 0 && n < MAX_PATH)
|
if (n != 0 && n < MAX_PATH) {
|
||||||
{
|
|
||||||
const std::wstring sentinel = std::wstring(dir) + L"coop_hook.log.on";
|
const std::wstring sentinel = std::wstring(dir) + L"coop_hook.log.on";
|
||||||
return GetFileAttributesW(sentinel.c_str()) != INVALID_FILE_ATTRIBUTES;
|
return GetFileAttributesW(sentinel.c_str()) != INVALID_FILE_ATTRIBUTES;
|
||||||
}
|
}
|
||||||
@@ -45,15 +41,12 @@ bool logging_enabled()
|
|||||||
|
|
||||||
FILE* log_file_locked()
|
FILE* log_file_locked()
|
||||||
{
|
{
|
||||||
if (!g_log_tried)
|
if (!g_log_tried) {
|
||||||
{
|
|
||||||
g_log_tried = true;
|
g_log_tried = true;
|
||||||
if (logging_enabled())
|
if (logging_enabled()) {
|
||||||
{
|
|
||||||
wchar_t dir[MAX_PATH] = {};
|
wchar_t dir[MAX_PATH] = {};
|
||||||
const DWORD n = GetTempPathW(MAX_PATH, dir);
|
const DWORD n = GetTempPathW(MAX_PATH, dir);
|
||||||
if (n != 0 && n < MAX_PATH)
|
if (n != 0 && n < MAX_PATH) {
|
||||||
{
|
|
||||||
std::wstring path = std::wstring(dir) + L"coop_hook.log";
|
std::wstring path = std::wstring(dir) + L"coop_hook.log";
|
||||||
g_log_file = _wfopen(path.c_str(), L"a");
|
g_log_file = _wfopen(path.c_str(), L"a");
|
||||||
}
|
}
|
||||||
@@ -69,12 +62,10 @@ void set_log_ring(coop::LogRing* ring)
|
|||||||
g_log_ring.store(ring, std::memory_order_release);
|
g_log_ring.store(ring, std::memory_order_release);
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
const char* level_tag(std::uint32_t level)
|
const char* level_tag(std::uint32_t level)
|
||||||
{
|
{
|
||||||
switch (level)
|
switch (level) {
|
||||||
{
|
|
||||||
case coop::LogLevel_Warn:
|
case coop::LogLevel_Warn:
|
||||||
return "WARN ";
|
return "WARN ";
|
||||||
case coop::LogLevel_Error:
|
case coop::LogLevel_Error:
|
||||||
@@ -91,20 +82,18 @@ void vlog(std::uint32_t level, const char* fmt, va_list args)
|
|||||||
std::vsnprintf(line, sizeof(line), fmt, args);
|
std::vsnprintf(line, sizeof(line), fmt, args);
|
||||||
|
|
||||||
// Stream to the host's Log window over the shared ring (the primary sink).
|
// Stream to the host's Log window over the shared ring (the primary sink).
|
||||||
if (coop::LogRing* ring = g_log_ring.load(std::memory_order_acquire))
|
if (coop::LogRing* ring = g_log_ring.load(std::memory_order_acquire)) {
|
||||||
{
|
|
||||||
coop::log_ring_push(*ring, GetCurrentProcessId(), level, GetTickCount64(), line);
|
coop::log_ring_push(*ring, GetCurrentProcessId(), level, GetTickCount64(), line);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also mirror to the file when the opt-in trace is enabled.
|
// Also mirror to the file when the opt-in trace is enabled.
|
||||||
std::scoped_lock lock(g_log_mutex);
|
std::scoped_lock lock(g_log_mutex);
|
||||||
FILE* f = log_file_locked();
|
FILE* f = log_file_locked();
|
||||||
if (f != nullptr)
|
if (f != nullptr) {
|
||||||
{
|
|
||||||
SYSTEMTIME st;
|
SYSTEMTIME st;
|
||||||
GetLocalTime(&st);
|
GetLocalTime(&st);
|
||||||
std::fprintf(f, "[%02u:%02u:%02u.%03u pid=%lu %s] %s\n", st.wHour, st.wMinute, st.wSecond,
|
std::fprintf(f, "[%02u:%02u:%02u.%03u pid=%lu %s] %s\n", st.wHour, st.wMinute, st.wSecond, st.wMilliseconds,
|
||||||
st.wMilliseconds, GetCurrentProcessId(), level_tag(level), line);
|
GetCurrentProcessId(), level_tag(level), line);
|
||||||
std::fflush(f);
|
std::fflush(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,11 @@
|
|||||||
// see debug_log.cpp). Thread-safe; cheap enough to leave compiled in.
|
// see debug_log.cpp). Thread-safe; cheap enough to leave compiled in.
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
struct LogRing;
|
struct LogRing;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
// Append a printf-style line to the log ring (if attached) and the file (if on).
|
// Append a printf-style line to the log ring (if attached) and the file (if on).
|
||||||
// logf = info, logw = warning, loge = error; the host colours the Log window by level.
|
// logf = info, logw = warning, loge = error; the host colours the Log window by level.
|
||||||
|
|||||||
@@ -27,8 +27,7 @@
|
|||||||
#include "vk_hook.hpp"
|
#include "vk_hook.hpp"
|
||||||
#include "xinput_hook.hpp"
|
#include "xinput_hook.hpp"
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
coop::hook::IpcClient g_ipc;
|
coop::hook::IpcClient g_ipc;
|
||||||
std::atomic<bool> g_running{true};
|
std::atomic<bool> g_running{true};
|
||||||
@@ -40,8 +39,7 @@ DWORD WINAPI worker_thread(LPVOID)
|
|||||||
coop::hook::logf("worker_thread: started");
|
coop::hook::logf("worker_thread: started");
|
||||||
|
|
||||||
// The host creates the mapping around injection time; give it a few seconds.
|
// The host creates the mapping around injection time; give it a few seconds.
|
||||||
if (!g_ipc.connect(/*attempts=*/200, /*delay_ms=*/25))
|
if (!g_ipc.connect(/*attempts=*/200, /*delay_ms=*/25)) {
|
||||||
{
|
|
||||||
coop::hook::logf("worker_thread: IPC connect FAILED (no host mapping); exiting");
|
coop::hook::logf("worker_thread: IPC connect FAILED (no host mapping); exiting");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -49,15 +47,11 @@ DWORD WINAPI worker_thread(LPVOID)
|
|||||||
// window. The host creates it at injection time; it's normally already there.
|
// window. The host creates it at injection time; it's normally already there.
|
||||||
{
|
{
|
||||||
const std::wstring log_name = coop::log_ring_name(GetCurrentProcessId());
|
const std::wstring log_name = coop::log_ring_name(GetCurrentProcessId());
|
||||||
if (g_log_shm.open(log_name, coop::log_ring_total_size(coop::kLogCapacity)))
|
if (g_log_shm.open(log_name, coop::log_ring_total_size(coop::kLogCapacity))) {
|
||||||
{
|
|
||||||
auto* lr = g_log_shm.as<coop::LogRing>();
|
auto* lr = g_log_shm.as<coop::LogRing>();
|
||||||
if (coop::log_ring_valid(*lr))
|
if (coop::log_ring_valid(*lr)) {
|
||||||
{
|
|
||||||
coop::hook::set_log_ring(lr);
|
coop::hook::set_log_ring(lr);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
g_log_shm.reset();
|
g_log_shm.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,28 +75,21 @@ DWORD WINAPI worker_thread(LPVOID)
|
|||||||
// what's wanted but missing (modules / the game window may appear lazily) and
|
// what's wanted but missing (modules / the game window may appear lazily) and
|
||||||
// remove what's no longer wanted (the host toggled it off). Beat a heartbeat so
|
// remove what's no longer wanted (the host toggled it off). Beat a heartbeat so
|
||||||
// the host can see the hook is alive.
|
// the host can see the hook is alive.
|
||||||
while (g_running.load(std::memory_order_relaxed))
|
while (g_running.load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
// --- Input (XInput) ---
|
// --- Input (XInput) ---
|
||||||
const bool want_input = g_ipc.subsystem_install_requested(coop::HookSubsys_Input);
|
const bool want_input = g_ipc.subsystem_install_requested(coop::HookSubsys_Input);
|
||||||
if (want_input && !xinput_installed)
|
if (want_input && !xinput_installed) {
|
||||||
{
|
|
||||||
xinput_installed = coop::hook::install_xinput_hooks(g_ipc);
|
xinput_installed = coop::hook::install_xinput_hooks(g_ipc);
|
||||||
}
|
} else if (!want_input && xinput_installed) {
|
||||||
else if (!want_input && xinput_installed)
|
|
||||||
{
|
|
||||||
coop::hook::remove_xinput_hooks();
|
coop::hook::remove_xinput_hooks();
|
||||||
xinput_installed = false;
|
xinput_installed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Focus spoof ---
|
// --- Focus spoof ---
|
||||||
const bool want_focus = g_ipc.subsystem_install_requested(coop::HookSubsys_Focus);
|
const bool want_focus = g_ipc.subsystem_install_requested(coop::HookSubsys_Focus);
|
||||||
if (want_focus && !focus_installed)
|
if (want_focus && !focus_installed) {
|
||||||
{
|
|
||||||
focus_installed = coop::hook::install_focus_spoof(g_ipc);
|
focus_installed = coop::hook::install_focus_spoof(g_ipc);
|
||||||
}
|
} else if (!want_focus && focus_installed) {
|
||||||
else if (!want_focus && focus_installed)
|
|
||||||
{
|
|
||||||
coop::hook::remove_focus_spoof();
|
coop::hook::remove_focus_spoof();
|
||||||
focus_installed = false;
|
focus_installed = false;
|
||||||
}
|
}
|
||||||
@@ -111,17 +98,13 @@ DWORD WINAPI worker_thread(LPVOID)
|
|||||||
// Install even before the host's ring exists so render streams are counted
|
// Install even before the host's ring exists so render streams are counted
|
||||||
// regardless; attach the ring (enabling capture+silence) once it appears.
|
// regardless; attach the ring (enabling capture+silence) once it appears.
|
||||||
const bool want_audio = com_ok && g_ipc.subsystem_install_requested(coop::HookSubsys_Audio);
|
const bool want_audio = com_ok && g_ipc.subsystem_install_requested(coop::HookSubsys_Audio);
|
||||||
if (want_audio && !audio_installed)
|
if (want_audio && !audio_installed) {
|
||||||
{
|
|
||||||
audio_installed = coop::hook::install_audio_hooks(g_ipc, nullptr);
|
audio_installed = coop::hook::install_audio_hooks(g_ipc, nullptr);
|
||||||
if (audio_installed)
|
if (audio_installed) {
|
||||||
{
|
|
||||||
coop::hook::logf("worker_thread: audio hooks installed");
|
coop::hook::logf("worker_thread: audio hooks installed");
|
||||||
audio_ring_open = false; // re-attach the ring below after a reinstall
|
audio_ring_open = false; // re-attach the ring below after a reinstall
|
||||||
}
|
}
|
||||||
}
|
} else if (!want_audio && audio_installed) {
|
||||||
else if (!want_audio && audio_installed)
|
|
||||||
{
|
|
||||||
coop::hook::remove_audio_hooks();
|
coop::hook::remove_audio_hooks();
|
||||||
audio_installed = false;
|
audio_installed = false;
|
||||||
audio_ring_open = false;
|
audio_ring_open = false;
|
||||||
@@ -133,31 +116,25 @@ DWORD WINAPI worker_thread(LPVOID)
|
|||||||
// Install both producers: DXGI games hit the Present hook, OpenGL games hit
|
// Install both producers: DXGI games hit the Present hook, OpenGL games hit
|
||||||
// the SwapBuffers hook, whichever the game uses fills the shared texture.
|
// the SwapBuffers hook, whichever the game uses fills the shared texture.
|
||||||
const bool want_video = g_ipc.subsystem_install_requested(coop::HookSubsys_Video);
|
const bool want_video = g_ipc.subsystem_install_requested(coop::HookSubsys_Video);
|
||||||
if (want_video && !video_installed)
|
if (want_video && !video_installed) {
|
||||||
{
|
|
||||||
const bool present_ok = coop::hook::install_present_hooks(g_ipc);
|
const bool present_ok = coop::hook::install_present_hooks(g_ipc);
|
||||||
const bool gl_ok = coop::hook::install_opengl_hooks(g_ipc);
|
const bool gl_ok = coop::hook::install_opengl_hooks(g_ipc);
|
||||||
const bool d3d9_ok = coop::hook::install_d3d9_hooks(g_ipc);
|
const bool d3d9_ok = coop::hook::install_d3d9_hooks(g_ipc);
|
||||||
video_installed = present_ok || gl_ok || d3d9_ok;
|
video_installed = present_ok || gl_ok || d3d9_ok;
|
||||||
if (video_installed)
|
if (video_installed) {
|
||||||
{
|
|
||||||
coop::hook::logf("worker_thread: video hooks installed (present=%d opengl=%d d3d9=%d)",
|
coop::hook::logf("worker_thread: video hooks installed (present=%d opengl=%d d3d9=%d)",
|
||||||
present_ok ? 1 : 0, gl_ok ? 1 : 0, d3d9_ok ? 1 : 0);
|
present_ok ? 1 : 0, gl_ok ? 1 : 0, d3d9_ok ? 1 : 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Vulkan separately: vulkan-1.dll loads lazily (volk dlopens it after start), so the DXGI/
|
// Vulkan separately: vulkan-1.dll loads lazily (volk dlopens it after start), so the DXGI/
|
||||||
// GL/D3D9 hooks above may install before it exists. Keep trying each tick until it appears.
|
// GL/D3D9 hooks above may install before it exists. Keep trying each tick until it appears.
|
||||||
if (want_video && !vk_installed)
|
if (want_video && !vk_installed) {
|
||||||
{
|
|
||||||
vk_installed = coop::hook::install_vk_hooks(g_ipc);
|
vk_installed = coop::hook::install_vk_hooks(g_ipc);
|
||||||
if (vk_installed)
|
if (vk_installed) {
|
||||||
{
|
|
||||||
video_installed = true; // a Vulkan-only game otherwise has no video hook installed
|
video_installed = true; // a Vulkan-only game otherwise has no video hook installed
|
||||||
coop::hook::logf("worker_thread: vulkan video hook installed");
|
coop::hook::logf("worker_thread: vulkan video hook installed");
|
||||||
}
|
}
|
||||||
}
|
} else if (!want_video && video_installed) {
|
||||||
else if (!want_video && video_installed)
|
|
||||||
{
|
|
||||||
coop::hook::remove_present_hooks();
|
coop::hook::remove_present_hooks();
|
||||||
coop::hook::remove_opengl_hooks();
|
coop::hook::remove_opengl_hooks();
|
||||||
coop::hook::remove_d3d9_hooks();
|
coop::hook::remove_d3d9_hooks();
|
||||||
@@ -171,16 +148,12 @@ DWORD WINAPI worker_thread(LPVOID)
|
|||||||
// Opt-in. When on, the host streams MKB events into the shared ring; we post
|
// Opt-in. When on, the host streams MKB events into the shared ring; we post
|
||||||
// them to the game and synthesize polling state. Drained at high rate below.
|
// them to the game and synthesize polling state. Drained at high rate below.
|
||||||
const bool want_mkb = g_ipc.subsystem_install_requested(coop::HookSubsys_Mkb);
|
const bool want_mkb = g_ipc.subsystem_install_requested(coop::HookSubsys_Mkb);
|
||||||
if (want_mkb && !mkb_installed)
|
if (want_mkb && !mkb_installed) {
|
||||||
{
|
|
||||||
mkb_installed = coop::hook::install_mkb_hooks(g_ipc);
|
mkb_installed = coop::hook::install_mkb_hooks(g_ipc);
|
||||||
if (mkb_installed)
|
if (mkb_installed) {
|
||||||
{
|
|
||||||
coop::hook::logf("worker_thread: MKB hooks installed");
|
coop::hook::logf("worker_thread: MKB hooks installed");
|
||||||
}
|
}
|
||||||
}
|
} else if (!want_mkb && mkb_installed) {
|
||||||
else if (!want_mkb && mkb_installed)
|
|
||||||
{
|
|
||||||
coop::hook::remove_mkb_hooks();
|
coop::hook::remove_mkb_hooks();
|
||||||
mkb_installed = false;
|
mkb_installed = false;
|
||||||
coop::hook::logf("worker_thread: MKB hooks removed (host request)");
|
coop::hook::logf("worker_thread: MKB hooks removed (host request)");
|
||||||
@@ -189,29 +162,21 @@ DWORD WINAPI worker_thread(LPVOID)
|
|||||||
// Attach a ring per stream. The host creates up to kMaxAudioStreams rings
|
// Attach a ring per stream. The host creates up to kMaxAudioStreams rings
|
||||||
// (coop_audio_<pid>[_<index>]); we open each as it appears and (re)attach it so
|
// (coop_audio_<pid>[_<index>]); we open each as it appears and (re)attach it so
|
||||||
// every stream is captured + silenced into its own ring for the host to mix.
|
// every stream is captured + silenced into its own ring for the host to mix.
|
||||||
if (audio_installed)
|
if (audio_installed) {
|
||||||
{
|
for (unsigned i = 0; i < coop::kMaxAudioStreams; ++i) {
|
||||||
for (unsigned i = 0; i < coop::kMaxAudioStreams; ++i)
|
if (!g_audio_shm[i].valid()) {
|
||||||
{
|
|
||||||
if (!g_audio_shm[i].valid())
|
|
||||||
{
|
|
||||||
g_audio_shm[i].open(coop::audio_ring_name(GetCurrentProcessId(), i),
|
g_audio_shm[i].open(coop::audio_ring_name(GetCurrentProcessId(), i),
|
||||||
coop::audio_ring_total_size(coop::kAudioRingCapacity));
|
coop::audio_ring_total_size(coop::kAudioRingCapacity));
|
||||||
}
|
}
|
||||||
if (g_audio_shm[i].valid())
|
if (g_audio_shm[i].valid()) {
|
||||||
{
|
|
||||||
auto* ring = g_audio_shm[i].as<coop::AudioRingHeader>();
|
auto* ring = g_audio_shm[i].as<coop::AudioRingHeader>();
|
||||||
if (coop::audio_ring_valid(*ring))
|
if (coop::audio_ring_valid(*ring)) {
|
||||||
{
|
|
||||||
coop::hook::set_audio_ring(i, ring); // idempotent re-attach
|
coop::hook::set_audio_ring(i, ring); // idempotent re-attach
|
||||||
if (i == 0 && !audio_ring_open)
|
if (i == 0 && !audio_ring_open) {
|
||||||
{
|
|
||||||
audio_ring_open = true;
|
audio_ring_open = true;
|
||||||
coop::hook::logf("worker_thread: audio ring 0 opened");
|
coop::hook::logf("worker_thread: audio ring 0 opened");
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
g_audio_shm[i].reset(); // present but not our contract; retry
|
g_audio_shm[i].reset(); // present but not our contract; retry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,8 +185,7 @@ DWORD WINAPI worker_thread(LPVOID)
|
|||||||
// A stream is often registered before its ring is attached (or the host re-inits
|
// A stream is often registered before its ring is attached (or the host re-inits
|
||||||
// a ring on a mirror re-toggle, clearing its format); keep formats published so
|
// a ring on a mirror re-toggle, clearing its format); keep formats published so
|
||||||
// the host consumes the rings instead of falling back to loopback.
|
// the host consumes the rings instead of falling back to loopback.
|
||||||
if (audio_ring_open)
|
if (audio_ring_open) {
|
||||||
{
|
|
||||||
coop::hook::republish_audio_format();
|
coop::hook::republish_audio_format();
|
||||||
}
|
}
|
||||||
coop::hook::update_input_diagnostics(g_ipc); // refreshes each tick; registrations can change
|
coop::hook::update_input_diagnostics(g_ipc); // refreshes each tick; registrations can change
|
||||||
@@ -232,18 +196,15 @@ DWORD WINAPI worker_thread(LPVOID)
|
|||||||
|
|
||||||
// Reconcile ~4x/s (50 slices x 5 ms), but drain MKB events every slice --
|
// Reconcile ~4x/s (50 slices x 5 ms), but drain MKB events every slice --
|
||||||
// input must stay responsive at a far higher rate than the reconcile.
|
// input must stay responsive at a far higher rate than the reconcile.
|
||||||
for (int slice = 0; slice < 50 && g_running.load(std::memory_order_relaxed); ++slice)
|
for (int slice = 0; slice < 50 && g_running.load(std::memory_order_relaxed); ++slice) {
|
||||||
{
|
if (mkb_installed) {
|
||||||
if (mkb_installed)
|
|
||||||
{
|
|
||||||
coop::hook::mkb_pump(g_ipc);
|
coop::hook::mkb_pump(g_ipc);
|
||||||
}
|
}
|
||||||
Sleep(5);
|
Sleep(5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (com_ok)
|
if (com_ok) {
|
||||||
{
|
|
||||||
CoUninitialize();
|
CoUninitialize();
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -253,20 +214,17 @@ DWORD WINAPI worker_thread(LPVOID)
|
|||||||
|
|
||||||
BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved)
|
BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved)
|
||||||
{
|
{
|
||||||
switch (reason)
|
switch (reason) {
|
||||||
{
|
|
||||||
case DLL_PROCESS_ATTACH:
|
case DLL_PROCESS_ATTACH:
|
||||||
DisableThreadLibraryCalls(module);
|
DisableThreadLibraryCalls(module);
|
||||||
if (HANDLE thread = CreateThread(nullptr, 0, &worker_thread, nullptr, 0, nullptr))
|
if (HANDLE thread = CreateThread(nullptr, 0, &worker_thread, nullptr, 0, nullptr)) {
|
||||||
{
|
|
||||||
CloseHandle(thread);
|
CloseHandle(thread);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case DLL_PROCESS_DETACH:
|
case DLL_PROCESS_DETACH:
|
||||||
// Skip cleanup when the process is tearing down (reserved != null): the
|
// Skip cleanup when the process is tearing down (reserved != null): the
|
||||||
// loader is already unwinding and touching other modules is unsafe.
|
// loader is already unwinding and touching other modules is unsafe.
|
||||||
if (reserved == nullptr)
|
if (reserved == nullptr) {
|
||||||
{
|
|
||||||
g_running.store(false, std::memory_order_relaxed);
|
g_running.store(false, std::memory_order_relaxed);
|
||||||
coop::hook::set_log_ring(nullptr);
|
coop::hook::set_log_ring(nullptr);
|
||||||
coop::hook::remove_focus_spoof();
|
coop::hook::remove_focus_spoof();
|
||||||
|
|||||||
@@ -5,13 +5,11 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
inline HWND find_main_window(DWORD pid)
|
inline HWND find_main_window(DWORD pid)
|
||||||
{
|
{
|
||||||
struct Ctx
|
struct Ctx {
|
||||||
{
|
|
||||||
DWORD pid;
|
DWORD pid;
|
||||||
HWND best;
|
HWND best;
|
||||||
long best_area;
|
long best_area;
|
||||||
@@ -22,18 +20,15 @@ inline HWND find_main_window(DWORD pid)
|
|||||||
auto* c = reinterpret_cast<Ctx*>(lparam);
|
auto* c = reinterpret_cast<Ctx*>(lparam);
|
||||||
DWORD pid = 0;
|
DWORD pid = 0;
|
||||||
GetWindowThreadProcessId(hwnd, &pid);
|
GetWindowThreadProcessId(hwnd, &pid);
|
||||||
if (pid != c->pid || !IsWindowVisible(hwnd) || GetWindow(hwnd, GW_OWNER) != nullptr)
|
if (pid != c->pid || !IsWindowVisible(hwnd) || GetWindow(hwnd, GW_OWNER) != nullptr) {
|
||||||
{
|
|
||||||
return TRUE; // not ours, hidden, or an owned dialog -- keep looking
|
return TRUE; // not ours, hidden, or an owned dialog -- keep looking
|
||||||
}
|
}
|
||||||
RECT rect = {};
|
RECT rect = {};
|
||||||
if (!GetWindowRect(hwnd, &rect))
|
if (!GetWindowRect(hwnd, &rect)) {
|
||||||
{
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
const long area = (rect.right - rect.left) * (rect.bottom - rect.top);
|
const long area = (rect.right - rect.left) * (rect.bottom - rect.top);
|
||||||
if (area > c->best_area)
|
if (area > c->best_area) {
|
||||||
{
|
|
||||||
c->best_area = area;
|
c->best_area = area;
|
||||||
c->best = hwnd;
|
c->best = hwnd;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,9 @@
|
|||||||
#include "hook_install.hpp"
|
#include "hook_install.hpp"
|
||||||
#include "hook_registry.hpp"
|
#include "hook_registry.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
DetourGate g_gate; // drains in-flight focus / WNDPROC detours before remove nulls their state
|
DetourGate g_gate; // drains in-flight focus / WNDPROC detours before remove nulls their state
|
||||||
|
|
||||||
@@ -40,11 +38,9 @@ safetyhook::InlineHook g_hk_setcursorpos;
|
|||||||
LRESULT CALLBACK subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
|
LRESULT CALLBACK subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
|
||||||
{
|
{
|
||||||
DetourGate::Guard guard(g_gate); // keep g_orig_proc / g_unicode valid for this whole dispatch
|
DetourGate::Guard guard(g_gate); // keep g_orig_proc / g_unicode valid for this whole dispatch
|
||||||
switch (msg)
|
switch (msg) {
|
||||||
{
|
|
||||||
case WM_ACTIVATE:
|
case WM_ACTIVATE:
|
||||||
if (LOWORD(wparam) == WA_INACTIVE)
|
if (LOWORD(wparam) == WA_INACTIVE) {
|
||||||
{
|
|
||||||
wparam = MAKEWPARAM(WA_ACTIVE, HIWORD(wparam));
|
wparam = MAKEWPARAM(WA_ACTIVE, HIWORD(wparam));
|
||||||
hook_note_call(g_id_wndproc);
|
hook_note_call(g_id_wndproc);
|
||||||
}
|
}
|
||||||
@@ -66,8 +62,7 @@ LRESULT CALLBACK subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam
|
|||||||
// Read g_orig_proc once; if the subclass is live but the original isn't published yet (the tiny
|
// Read g_orig_proc once; if the subclass is live but the original isn't published yet (the tiny
|
||||||
// install/remove window), fall back to DefWindowProc rather than call through a null pointer.
|
// install/remove window), fall back to DefWindowProc rather than call through a null pointer.
|
||||||
const WNDPROC orig = g_orig_proc;
|
const WNDPROC orig = g_orig_proc;
|
||||||
if (orig == nullptr)
|
if (orig == nullptr) {
|
||||||
{
|
|
||||||
return g_unicode ? DefWindowProcW(hwnd, msg, wparam, lparam) : DefWindowProcA(hwnd, msg, wparam, lparam);
|
return g_unicode ? DefWindowProcW(hwnd, msg, wparam, lparam) : DefWindowProcA(hwnd, msg, wparam, lparam);
|
||||||
}
|
}
|
||||||
return g_unicode ? CallWindowProcW(orig, hwnd, msg, wparam, lparam)
|
return g_unicode ? CallWindowProcW(orig, hwnd, msg, wparam, lparam)
|
||||||
@@ -78,8 +73,7 @@ HWND WINAPI hk_GetForegroundWindow()
|
|||||||
{
|
{
|
||||||
DetourGate::Guard guard(g_gate);
|
DetourGate::Guard guard(g_gate);
|
||||||
hook_note_call(g_id_foreground);
|
hook_note_call(g_id_foreground);
|
||||||
if (g_focus_ipc != nullptr)
|
if (g_focus_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_focus_ipc->note_focus_query(FocusApi_Foreground);
|
g_focus_ipc->note_focus_query(FocusApi_Foreground);
|
||||||
}
|
}
|
||||||
return g_game_hwnd;
|
return g_game_hwnd;
|
||||||
@@ -89,8 +83,7 @@ HWND WINAPI hk_GetActiveWindow()
|
|||||||
{
|
{
|
||||||
DetourGate::Guard guard(g_gate);
|
DetourGate::Guard guard(g_gate);
|
||||||
hook_note_call(g_id_active);
|
hook_note_call(g_id_active);
|
||||||
if (g_focus_ipc != nullptr)
|
if (g_focus_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_focus_ipc->note_focus_query(FocusApi_Active);
|
g_focus_ipc->note_focus_query(FocusApi_Active);
|
||||||
}
|
}
|
||||||
return g_game_hwnd;
|
return g_game_hwnd;
|
||||||
@@ -100,8 +93,7 @@ HWND WINAPI hk_GetFocus()
|
|||||||
{
|
{
|
||||||
DetourGate::Guard guard(g_gate);
|
DetourGate::Guard guard(g_gate);
|
||||||
hook_note_call(g_id_focus);
|
hook_note_call(g_id_focus);
|
||||||
if (g_focus_ipc != nullptr)
|
if (g_focus_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_focus_ipc->note_focus_query(FocusApi_Focus);
|
g_focus_ipc->note_focus_query(FocusApi_Focus);
|
||||||
}
|
}
|
||||||
return g_game_hwnd;
|
return g_game_hwnd;
|
||||||
@@ -124,8 +116,7 @@ BOOL WINAPI hk_SetCursorPos(int x, int y)
|
|||||||
DetourGate::Guard guard(g_gate);
|
DetourGate::Guard guard(g_gate);
|
||||||
hook_note_call(g_id_setcursorpos);
|
hook_note_call(g_id_setcursorpos);
|
||||||
const bool allow = g_focus_ipc != nullptr && g_focus_ipc->cursor_clip_allowed();
|
const bool allow = g_focus_ipc != nullptr && g_focus_ipc->cursor_clip_allowed();
|
||||||
if (!allow)
|
if (!allow) {
|
||||||
{
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
return g_hk_setcursorpos.stdcall<BOOL>(x, y);
|
return g_hk_setcursorpos.stdcall<BOOL>(x, y);
|
||||||
@@ -133,8 +124,7 @@ BOOL WINAPI hk_SetCursorPos(int x, int y)
|
|||||||
|
|
||||||
void hook_export(HMODULE module, const char* name, void* detour, int registry_id)
|
void hook_export(HMODULE module, const char* name, void* detour, int registry_id)
|
||||||
{
|
{
|
||||||
if (void* target = reinterpret_cast<void*>(GetProcAddress(module, name)))
|
if (void* target = reinterpret_cast<void*>(GetProcAddress(module, name))) {
|
||||||
{
|
|
||||||
g_focus_hooks.emplace_back();
|
g_focus_hooks.emplace_back();
|
||||||
install_inline(g_focus_hooks.back(), target, detour); // assign-then-enable (no install race)
|
install_inline(g_focus_hooks.back(), target, detour); // assign-then-enable (no install race)
|
||||||
hook_set_installed(registry_id, true);
|
hook_set_installed(registry_id, true);
|
||||||
@@ -146,8 +136,7 @@ void hook_export(HMODULE module, const char* name, void* detour, int registry_id
|
|||||||
bool install_focus_spoof(IpcClient& ipc)
|
bool install_focus_spoof(IpcClient& ipc)
|
||||||
{
|
{
|
||||||
g_focus_ipc = &ipc;
|
g_focus_ipc = &ipc;
|
||||||
if (g_game_hwnd != nullptr)
|
if (g_game_hwnd != nullptr) {
|
||||||
{
|
|
||||||
return true; // already active
|
return true; // already active
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,8 +148,7 @@ bool install_focus_spoof(IpcClient& ipc)
|
|||||||
g_id_setcursorpos = hook_register("SetCursorPos (cursor release)", HookSubsys_Focus);
|
g_id_setcursorpos = hook_register("SetCursorPos (cursor release)", HookSubsys_Focus);
|
||||||
|
|
||||||
HWND hwnd = find_main_window(GetCurrentProcessId());
|
HWND hwnd = find_main_window(GetCurrentProcessId());
|
||||||
if (hwnd == nullptr)
|
if (hwnd == nullptr) {
|
||||||
{
|
|
||||||
return false; // window not created yet; caller retries
|
return false; // window not created yet; caller retries
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,38 +161,30 @@ bool install_focus_spoof(IpcClient& ipc)
|
|||||||
// thread is safe (the new proc runs on the window's own thread); match A/W for CallWindowProc.
|
// thread is safe (the new proc runs on the window's own thread); match A/W for CallWindowProc.
|
||||||
g_orig_proc = g_unicode ? reinterpret_cast<WNDPROC>(GetWindowLongPtrW(hwnd, GWLP_WNDPROC))
|
g_orig_proc = g_unicode ? reinterpret_cast<WNDPROC>(GetWindowLongPtrW(hwnd, GWLP_WNDPROC))
|
||||||
: reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hwnd, GWLP_WNDPROC));
|
: reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hwnd, GWLP_WNDPROC));
|
||||||
if (g_unicode)
|
if (g_unicode) {
|
||||||
{
|
|
||||||
SetWindowLongPtrW(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&subclass_proc));
|
SetWindowLongPtrW(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&subclass_proc));
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
SetWindowLongPtrA(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&subclass_proc));
|
SetWindowLongPtrA(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&subclass_proc));
|
||||||
}
|
}
|
||||||
hook_set_installed(g_id_wndproc, true);
|
hook_set_installed(g_id_wndproc, true);
|
||||||
|
|
||||||
if (HMODULE user32 = GetModuleHandleW(L"user32.dll"))
|
if (HMODULE user32 = GetModuleHandleW(L"user32.dll")) {
|
||||||
{
|
hook_export(user32, "GetForegroundWindow", reinterpret_cast<void*>(&hk_GetForegroundWindow), g_id_foreground);
|
||||||
hook_export(user32, "GetForegroundWindow", reinterpret_cast<void*>(&hk_GetForegroundWindow),
|
|
||||||
g_id_foreground);
|
|
||||||
hook_export(user32, "GetActiveWindow", reinterpret_cast<void*>(&hk_GetActiveWindow), g_id_active);
|
hook_export(user32, "GetActiveWindow", reinterpret_cast<void*>(&hk_GetActiveWindow), g_id_active);
|
||||||
hook_export(user32, "GetFocus", reinterpret_cast<void*>(&hk_GetFocus), g_id_focus);
|
hook_export(user32, "GetFocus", reinterpret_cast<void*>(&hk_GetFocus), g_id_focus);
|
||||||
|
|
||||||
if (void* clip = reinterpret_cast<void*>(GetProcAddress(user32, "ClipCursor")))
|
if (void* clip = reinterpret_cast<void*>(GetProcAddress(user32, "ClipCursor"))) {
|
||||||
{
|
|
||||||
install_inline(g_hk_clipcursor, clip, &hk_ClipCursor);
|
install_inline(g_hk_clipcursor, clip, &hk_ClipCursor);
|
||||||
hook_set_installed(g_id_clipcursor, static_cast<bool>(g_hk_clipcursor));
|
hook_set_installed(g_id_clipcursor, static_cast<bool>(g_hk_clipcursor));
|
||||||
}
|
}
|
||||||
if (void* setpos = reinterpret_cast<void*>(GetProcAddress(user32, "SetCursorPos")))
|
if (void* setpos = reinterpret_cast<void*>(GetProcAddress(user32, "SetCursorPos"))) {
|
||||||
{
|
|
||||||
install_inline(g_hk_setcursorpos, setpos, &hk_SetCursorPos);
|
install_inline(g_hk_setcursorpos, setpos, &hk_SetCursorPos);
|
||||||
hook_set_installed(g_id_setcursorpos, static_cast<bool>(g_hk_setcursorpos));
|
hook_set_installed(g_id_setcursorpos, static_cast<bool>(g_hk_setcursorpos));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free any clip the game already set, so release takes effect immediately.
|
// Free any clip the game already set, so release takes effect immediately.
|
||||||
if (!ipc.cursor_clip_allowed())
|
if (!ipc.cursor_clip_allowed()) {
|
||||||
{
|
|
||||||
ClipCursor(nullptr);
|
ClipCursor(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,20 +201,16 @@ void update_input_diagnostics(IpcClient& ipc)
|
|||||||
bool raw_gamepad_sink = false;
|
bool raw_gamepad_sink = false;
|
||||||
|
|
||||||
UINT count = 0;
|
UINT count = 0;
|
||||||
if (GetRegisteredRawInputDevices(nullptr, &count, sizeof(RAWINPUTDEVICE)) == 0 && count > 0)
|
if (GetRegisteredRawInputDevices(nullptr, &count, sizeof(RAWINPUTDEVICE)) == 0 && count > 0) {
|
||||||
{
|
|
||||||
std::vector<RAWINPUTDEVICE> devices(count);
|
std::vector<RAWINPUTDEVICE> devices(count);
|
||||||
const UINT got = GetRegisteredRawInputDevices(devices.data(), &count, sizeof(RAWINPUTDEVICE));
|
const UINT got = GetRegisteredRawInputDevices(devices.data(), &count, sizeof(RAWINPUTDEVICE));
|
||||||
if (got != static_cast<UINT>(-1))
|
if (got != static_cast<UINT>(-1)) {
|
||||||
{
|
|
||||||
raw_registered = got > 0;
|
raw_registered = got > 0;
|
||||||
for (UINT i = 0; i < got; ++i)
|
for (UINT i = 0; i < got; ++i) {
|
||||||
{
|
|
||||||
// Generic Desktop (0x01) joystick (0x04) / gamepad (0x05).
|
// Generic Desktop (0x01) joystick (0x04) / gamepad (0x05).
|
||||||
const bool is_pad =
|
const bool is_pad =
|
||||||
devices[i].usUsagePage == 0x01 && (devices[i].usUsage == 0x04 || devices[i].usUsage == 0x05);
|
devices[i].usUsagePage == 0x01 && (devices[i].usUsage == 0x04 || devices[i].usUsage == 0x05);
|
||||||
if (is_pad)
|
if (is_pad) {
|
||||||
{
|
|
||||||
raw_gamepad = true;
|
raw_gamepad = true;
|
||||||
raw_gamepad_sink = (devices[i].dwFlags & RIDEV_INPUTSINK) != 0;
|
raw_gamepad_sink = (devices[i].dwFlags & RIDEV_INPUTSINK) != 0;
|
||||||
}
|
}
|
||||||
@@ -248,22 +224,17 @@ void update_input_diagnostics(IpcClient& ipc)
|
|||||||
|
|
||||||
void release_cursor_tick()
|
void release_cursor_tick()
|
||||||
{
|
{
|
||||||
if (g_focus_ipc != nullptr && g_game_hwnd != nullptr && !g_focus_ipc->cursor_clip_allowed())
|
if (g_focus_ipc != nullptr && g_game_hwnd != nullptr && !g_focus_ipc->cursor_clip_allowed()) {
|
||||||
{
|
|
||||||
ClipCursor(nullptr); // routes through hk_ClipCursor -> frees the cursor
|
ClipCursor(nullptr); // routes through hk_ClipCursor -> frees the cursor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void remove_focus_spoof()
|
void remove_focus_spoof()
|
||||||
{
|
{
|
||||||
if (g_game_hwnd != nullptr && g_orig_proc != nullptr)
|
if (g_game_hwnd != nullptr && g_orig_proc != nullptr) {
|
||||||
{
|
if (g_unicode) {
|
||||||
if (g_unicode)
|
|
||||||
{
|
|
||||||
SetWindowLongPtrW(g_game_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(g_orig_proc));
|
SetWindowLongPtrW(g_game_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(g_orig_proc));
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
SetWindowLongPtrA(g_game_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(g_orig_proc));
|
SetWindowLongPtrA(g_game_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(g_orig_proc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,8 +246,7 @@ void remove_focus_spoof()
|
|||||||
// patched bytes. The reverse of the enable order (GFW first) keeps the invariant "GetActiveWindow
|
// patched bytes. The reverse of the enable order (GFW first) keeps the invariant "GetActiveWindow
|
||||||
// hooked => GetForegroundWindow hooked" across the whole install/remove cycle, so a call never
|
// hooked => GetForegroundWindow hooked" across the whole install/remove cycle, so a call never
|
||||||
// lands in a half-patched shared region.
|
// lands in a half-patched shared region.
|
||||||
for (auto it = g_focus_hooks.rbegin(); it != g_focus_hooks.rend(); ++it)
|
for (auto it = g_focus_hooks.rbegin(); it != g_focus_hooks.rend(); ++it) {
|
||||||
{
|
|
||||||
disable_for_removal(*it);
|
disable_for_removal(*it);
|
||||||
}
|
}
|
||||||
disable_for_removal(g_hk_clipcursor);
|
disable_for_removal(g_hk_clipcursor);
|
||||||
@@ -298,8 +268,7 @@ void remove_focus_spoof()
|
|||||||
// DO call the trampoline, so keep them ALIVE (disabled) -- persistent, re-enabled on re-install
|
// DO call the trampoline, so keep them ALIVE (disabled) -- persistent, re-enabled on re-install
|
||||||
// (see hook_install.hpp) -- so a stale detour never hits a freed trampoline.
|
// (see hook_install.hpp) -- so a stale detour never hits a freed trampoline.
|
||||||
g_focus_hooks.clear();
|
g_focus_hooks.clear();
|
||||||
if (g_focus_ipc != nullptr)
|
if (g_focus_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_focus_ipc->mark_focus_spoof(false, 0);
|
g_focus_ipc->mark_focus_spoof(false, 0);
|
||||||
}
|
}
|
||||||
g_game_hwnd = nullptr;
|
g_game_hwnd = nullptr;
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
|
|
||||||
#include "ipc_client.hpp"
|
#include "ipc_client.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
// Finds the game's main window, subclasses it to suppress deactivation messages,
|
// Finds the game's main window, subclasses it to suppress deactivation messages,
|
||||||
// and hooks the focus-query APIs to always report the game as active. Returns
|
// and hooks the focus-query APIs to always report the game as active. Returns
|
||||||
|
|||||||
@@ -26,24 +26,15 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
class DetourGate
|
class DetourGate {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
// RAII: marks a detour body as in-flight for as long as it's on the stack.
|
// RAII: marks a detour body as in-flight for as long as it's on the stack.
|
||||||
class Guard
|
class Guard {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
explicit Guard(DetourGate& gate) : m_gate(gate)
|
explicit Guard(DetourGate& gate) : m_gate(gate) { m_gate.m_active.fetch_add(1, std::memory_order_acq_rel); }
|
||||||
{
|
~Guard() { m_gate.m_active.fetch_sub(1, std::memory_order_acq_rel); }
|
||||||
m_gate.m_active.fetch_add(1, std::memory_order_acq_rel);
|
|
||||||
}
|
|
||||||
~Guard()
|
|
||||||
{
|
|
||||||
m_gate.m_active.fetch_sub(1, std::memory_order_acq_rel);
|
|
||||||
}
|
|
||||||
Guard(const Guard&) = delete;
|
Guard(const Guard&) = delete;
|
||||||
Guard& operator=(const Guard&) = delete;
|
Guard& operator=(const Guard&) = delete;
|
||||||
|
|
||||||
@@ -64,20 +55,15 @@ public:
|
|||||||
// reliably, so checking before the first sleep is not safe.
|
// reliably, so checking before the first sleep is not safe.
|
||||||
void drain()
|
void drain()
|
||||||
{
|
{
|
||||||
for (int spins = 0; spins < 400; ++spins)
|
for (int spins = 0; spins < 400; ++spins) {
|
||||||
{
|
|
||||||
Sleep(1);
|
Sleep(1);
|
||||||
if (m_active.load(std::memory_order_acquire) == 0)
|
if (m_active.load(std::memory_order_acquire) == 0) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int active() const
|
int active() const { return m_active.load(std::memory_order_acquire); }
|
||||||
{
|
|
||||||
return m_active.load(std::memory_order_acquire);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::atomic<int> m_active{0};
|
std::atomic<int> m_active{0};
|
||||||
@@ -92,8 +78,7 @@ private:
|
|||||||
template <class InlineHook>
|
template <class InlineHook>
|
||||||
void disable_for_removal(InlineHook& hook)
|
void disable_for_removal(InlineHook& hook)
|
||||||
{
|
{
|
||||||
if (!hook.disable())
|
if (!hook.disable()) {
|
||||||
{
|
|
||||||
OutputDebugStringA("coop: SafetyHook InlineHook::disable() failed during removal -- unhook may be unsafe\n");
|
OutputDebugStringA("coop: SafetyHook InlineHook::disable() failed during removal -- unhook may be unsafe\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,7 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
// Arm `detour` over `target` in `dst`: create it once (StartDisabled) if empty, then enable. Calling
|
// Arm `detour` over `target` in `dst`: create it once (StartDisabled) if empty, then enable. Calling
|
||||||
// this again after a remove just re-enables the SAME hook (no recreate -> the trampoline is never
|
// this again after a remove just re-enables the SAME hook (no recreate -> the trampoline is never
|
||||||
@@ -36,8 +35,7 @@ inline void install_inline(safetyhook::InlineHook& dst, void* target, void* deto
|
|||||||
{
|
{
|
||||||
dst = safetyhook::create_inline(target, detour, safetyhook::InlineHook::StartDisabled);
|
dst = safetyhook::create_inline(target, detour, safetyhook::InlineHook::StartDisabled);
|
||||||
}
|
}
|
||||||
if (dst && !dst.enable())
|
if (dst && !dst.enable()) {
|
||||||
{
|
|
||||||
OutputDebugStringA("coop: SafetyHook InlineHook::enable() failed during install\n");
|
OutputDebugStringA("coop: SafetyHook InlineHook::enable() failed during install\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,11 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
struct Slot
|
struct Slot {
|
||||||
{
|
|
||||||
char name[40] = {};
|
char name[40] = {};
|
||||||
std::atomic<std::uint32_t> subsystem{0};
|
std::atomic<std::uint32_t> subsystem{0};
|
||||||
std::atomic<std::uint32_t> installed{0};
|
std::atomic<std::uint32_t> installed{0};
|
||||||
@@ -29,15 +26,12 @@ int hook_register(const char* name, std::uint32_t subsystem)
|
|||||||
{
|
{
|
||||||
std::scoped_lock lock(g_register_mutex);
|
std::scoped_lock lock(g_register_mutex);
|
||||||
const std::uint32_t count = g_count.load(std::memory_order_relaxed);
|
const std::uint32_t count = g_count.load(std::memory_order_relaxed);
|
||||||
for (std::uint32_t i = 0; i < count; ++i)
|
for (std::uint32_t i = 0; i < count; ++i) {
|
||||||
{
|
if (g_slots[i].used.load(std::memory_order_relaxed) && std::strcmp(g_slots[i].name, name) == 0) {
|
||||||
if (g_slots[i].used.load(std::memory_order_relaxed) && std::strcmp(g_slots[i].name, name) == 0)
|
|
||||||
{
|
|
||||||
return static_cast<int>(i); // already registered
|
return static_cast<int>(i); // already registered
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (count >= kMaxHookEntries)
|
if (count >= kMaxHookEntries) {
|
||||||
{
|
|
||||||
return -1; // table full
|
return -1; // table full
|
||||||
}
|
}
|
||||||
Slot& s = g_slots[count];
|
Slot& s = g_slots[count];
|
||||||
@@ -53,16 +47,14 @@ int hook_register(const char* name, std::uint32_t subsystem)
|
|||||||
|
|
||||||
void hook_set_installed(int id, bool installed)
|
void hook_set_installed(int id, bool installed)
|
||||||
{
|
{
|
||||||
if (id >= 0 && id < static_cast<int>(kMaxHookEntries))
|
if (id >= 0 && id < static_cast<int>(kMaxHookEntries)) {
|
||||||
{
|
|
||||||
g_slots[id].installed.store(installed ? 1u : 0u, std::memory_order_relaxed);
|
g_slots[id].installed.store(installed ? 1u : 0u, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void hook_note_call(int id)
|
void hook_note_call(int id)
|
||||||
{
|
{
|
||||||
if (id >= 0 && id < static_cast<int>(kMaxHookEntries))
|
if (id >= 0 && id < static_cast<int>(kMaxHookEntries)) {
|
||||||
{
|
|
||||||
g_slots[id].calls.fetch_add(1, std::memory_order_relaxed);
|
g_slots[id].calls.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,10 +64,8 @@ void hook_publish(IpcClient& ipc)
|
|||||||
const std::uint32_t count = g_count.load(std::memory_order_acquire);
|
const std::uint32_t count = g_count.load(std::memory_order_acquire);
|
||||||
HookEntry entries[kMaxHookEntries];
|
HookEntry entries[kMaxHookEntries];
|
||||||
std::uint32_t n = 0;
|
std::uint32_t n = 0;
|
||||||
for (std::uint32_t i = 0; i < count && i < kMaxHookEntries; ++i)
|
for (std::uint32_t i = 0; i < count && i < kMaxHookEntries; ++i) {
|
||||||
{
|
if (!g_slots[i].used.load(std::memory_order_acquire)) {
|
||||||
if (!g_slots[i].used.load(std::memory_order_acquire))
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
HookEntry& e = entries[n];
|
HookEntry& e = entries[n];
|
||||||
@@ -91,8 +81,7 @@ void hook_publish(IpcClient& ipc)
|
|||||||
void hook_registry_reset()
|
void hook_registry_reset()
|
||||||
{
|
{
|
||||||
std::scoped_lock lock(g_register_mutex);
|
std::scoped_lock lock(g_register_mutex);
|
||||||
for (auto& s : g_slots)
|
for (auto& s : g_slots) {
|
||||||
{
|
|
||||||
s.used.store(0, std::memory_order_relaxed);
|
s.used.store(0, std::memory_order_relaxed);
|
||||||
s.installed.store(0, std::memory_order_relaxed);
|
s.installed.store(0, std::memory_order_relaxed);
|
||||||
s.calls.store(0, std::memory_order_relaxed);
|
s.calls.store(0, std::memory_order_relaxed);
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
#include "coop/protocol.hpp"
|
#include "coop/protocol.hpp"
|
||||||
#include "ipc_client.hpp"
|
#include "ipc_client.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
// Find-or-create a registry slot for `name` in `subsystem`; returns a stable id
|
// Find-or-create a registry slot for `name` in `subsystem`; returns a stable id
|
||||||
// (>= 0) used with the calls below, or -1 if the table is full. Idempotent: the
|
// (>= 0) used with the calls below, or -1 if the table is full. Idempotent: the
|
||||||
|
|||||||
@@ -11,24 +11,19 @@
|
|||||||
#include "coop/protocol.hpp"
|
#include "coop/protocol.hpp"
|
||||||
#include "coop/shared_memory.hpp"
|
#include "coop/shared_memory.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
class IpcClient
|
class IpcClient {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
// Tries to open the section a few times: the host may inject us slightly
|
// Tries to open the section a few times: the host may inject us slightly
|
||||||
// before (or after) it creates the mapping. Returns true once connected.
|
// before (or after) it creates the mapping. Returns true once connected.
|
||||||
bool connect(int attempts, int delay_ms)
|
bool connect(int attempts, int delay_ms)
|
||||||
{
|
{
|
||||||
const std::wstring name = shared_memory_name(GetCurrentProcessId());
|
const std::wstring name = shared_memory_name(GetCurrentProcessId());
|
||||||
for (int i = 0; i < attempts; ++i)
|
for (int i = 0; i < attempts; ++i) {
|
||||||
{
|
if (shm_.open(name, sizeof(SharedBlock))) {
|
||||||
if (shm_.open(name, sizeof(SharedBlock)))
|
|
||||||
{
|
|
||||||
auto* block = shm_.as<SharedBlock>();
|
auto* block = shm_.as<SharedBlock>();
|
||||||
if (block->magic == kProtocolMagic && block->version == kProtocolVersion)
|
if (block->magic == kProtocolMagic && block->version == kProtocolVersion) {
|
||||||
{
|
|
||||||
block_ = block;
|
block_ = block;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -39,17 +34,13 @@ public:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool connected() const
|
[[nodiscard]] bool connected() const { return block_ != nullptr; }
|
||||||
{
|
|
||||||
return block_ != nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Host-requested install state for a subsystem (default = install, since the
|
// Host-requested install state for a subsystem (default = install, since the
|
||||||
// mapping is zero-filled and 0 means "disabled flag clear" = install).
|
// mapping is zero-filled and 0 means "disabled flag clear" = install).
|
||||||
[[nodiscard]] bool subsystem_install_requested(std::uint32_t subsystem) const
|
[[nodiscard]] bool subsystem_install_requested(std::uint32_t subsystem) const
|
||||||
{
|
{
|
||||||
if (block_ == nullptr || subsystem >= HookSubsys_Count)
|
if (block_ == nullptr || subsystem >= HookSubsys_Count) {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return block_->control.subsystem_disabled[subsystem].load(std::memory_order_acquire) == 0;
|
return block_->control.subsystem_disabled[subsystem].load(std::memory_order_acquire) == 0;
|
||||||
@@ -66,8 +57,7 @@ public:
|
|||||||
// was mid-write for the whole spin window (caller should reuse its cache).
|
// was mid-write for the whole spin window (caller should reuse its cache).
|
||||||
bool snapshot(CoopPadState (&out)[kMaxPads], std::uint32_t& count) const
|
bool snapshot(CoopPadState (&out)[kMaxPads], std::uint32_t& count) const
|
||||||
{
|
{
|
||||||
if (block_ == nullptr)
|
if (block_ == nullptr) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return read_pads(*block_, out, count);
|
return read_pads(*block_, out, count);
|
||||||
@@ -78,32 +68,28 @@ public:
|
|||||||
// Record that the game queried a controller slot via XInputGetState/Ex.
|
// Record that the game queried a controller slot via XInputGetState/Ex.
|
||||||
void note_state_query(std::uint32_t user_index)
|
void note_state_query(std::uint32_t user_index)
|
||||||
{
|
{
|
||||||
if (block_ != nullptr && user_index < kMaxPads)
|
if (block_ != nullptr && user_index < kMaxPads) {
|
||||||
{
|
|
||||||
block_->status.get_state_calls[user_index].fetch_add(1, std::memory_order_relaxed);
|
block_->status.get_state_calls[user_index].fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void note_caps_query(std::uint32_t user_index)
|
void note_caps_query(std::uint32_t user_index)
|
||||||
{
|
{
|
||||||
if (block_ != nullptr && user_index < kMaxPads)
|
if (block_ != nullptr && user_index < kMaxPads) {
|
||||||
{
|
|
||||||
block_->status.get_caps_calls[user_index].fetch_add(1, std::memory_order_relaxed);
|
block_->status.get_caps_calls[user_index].fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void note_focus_query(FocusApi which)
|
void note_focus_query(FocusApi which)
|
||||||
{
|
{
|
||||||
if (block_ != nullptr && which < FocusApi_Count)
|
if (block_ != nullptr && which < FocusApi_Count) {
|
||||||
{
|
|
||||||
block_->status.focus_query_calls[which].fetch_add(1, std::memory_order_relaxed);
|
block_->status.focus_query_calls[which].fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void mark_attached()
|
void mark_attached()
|
||||||
{
|
{
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr) {
|
||||||
{
|
|
||||||
block_->status.game_pid = GetCurrentProcessId();
|
block_->status.game_pid = GetCurrentProcessId();
|
||||||
block_->status.attached = 1;
|
block_->status.attached = 1;
|
||||||
}
|
}
|
||||||
@@ -113,16 +99,14 @@ public:
|
|||||||
// the Controllers panel stops showing stale poll rates.
|
// the Controllers panel stops showing stale poll rates.
|
||||||
void mark_detached()
|
void mark_detached()
|
||||||
{
|
{
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr) {
|
||||||
{
|
|
||||||
block_->status.attached = 0;
|
block_->status.attached = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void mark_focus_spoof(bool active, std::uint64_t game_hwnd)
|
void mark_focus_spoof(bool active, std::uint64_t game_hwnd)
|
||||||
{
|
{
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr) {
|
||||||
{
|
|
||||||
block_->status.focus_spoof = active ? 1u : 0u;
|
block_->status.focus_spoof = active ? 1u : 0u;
|
||||||
block_->status.game_hwnd = game_hwnd;
|
block_->status.game_hwnd = game_hwnd;
|
||||||
}
|
}
|
||||||
@@ -130,8 +114,7 @@ public:
|
|||||||
|
|
||||||
void set_input_diagnostics(bool raw_registered, bool raw_gamepad, bool raw_gamepad_sink, bool dinput)
|
void set_input_diagnostics(bool raw_registered, bool raw_gamepad, bool raw_gamepad_sink, bool dinput)
|
||||||
{
|
{
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr) {
|
||||||
{
|
|
||||||
block_->status.raw_input_registered = raw_registered ? 1u : 0u;
|
block_->status.raw_input_registered = raw_registered ? 1u : 0u;
|
||||||
block_->status.raw_input_gamepad = raw_gamepad ? 1u : 0u;
|
block_->status.raw_input_gamepad = raw_gamepad ? 1u : 0u;
|
||||||
block_->status.raw_input_gamepad_sink = raw_gamepad_sink ? 1u : 0u;
|
block_->status.raw_input_gamepad_sink = raw_gamepad_sink ? 1u : 0u;
|
||||||
@@ -141,16 +124,14 @@ public:
|
|||||||
|
|
||||||
void heartbeat()
|
void heartbeat()
|
||||||
{
|
{
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr) {
|
||||||
{
|
|
||||||
block_->status.heartbeat.fetch_add(1, std::memory_order_relaxed);
|
block_->status.heartbeat.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_vk_too_late(bool too_late)
|
void set_vk_too_late(bool too_late)
|
||||||
{
|
{
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr) {
|
||||||
{
|
|
||||||
block_->status.vk_too_late = too_late ? 1u : 0u;
|
block_->status.vk_too_late = too_late ? 1u : 0u;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,8 +140,7 @@ public:
|
|||||||
// the guest's controller). Plain stores; the hook is the sole writer.
|
// the guest's controller). Plain stores; the hook is the sole writer.
|
||||||
void note_rumble(std::uint32_t slot, std::uint16_t left, std::uint16_t right)
|
void note_rumble(std::uint32_t slot, std::uint16_t left, std::uint16_t right)
|
||||||
{
|
{
|
||||||
if (block_ != nullptr && slot < kMaxPads)
|
if (block_ != nullptr && slot < kMaxPads) {
|
||||||
{
|
|
||||||
block_->status.rumble_left[slot] = left;
|
block_->status.rumble_left[slot] = left;
|
||||||
block_->status.rumble_right[slot] = right;
|
block_->status.rumble_right[slot] = right;
|
||||||
}
|
}
|
||||||
@@ -169,8 +149,7 @@ public:
|
|||||||
// Record the state the hook just returned to the game for a slot (round-trip view).
|
// Record the state the hook just returned to the game for a slot (round-trip view).
|
||||||
void note_read_state(std::uint32_t slot, const CoopPadState& state)
|
void note_read_state(std::uint32_t slot, const CoopPadState& state)
|
||||||
{
|
{
|
||||||
if (block_ != nullptr && slot < kMaxPads)
|
if (block_ != nullptr && slot < kMaxPads) {
|
||||||
{
|
|
||||||
block_->status.read_state[slot] = state;
|
block_->status.read_state[slot] = state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,8 +159,7 @@ public:
|
|||||||
// Total distinct render streams the audio hook has observed.
|
// Total distinct render streams the audio hook has observed.
|
||||||
void set_audio_streams_seen(std::uint32_t count)
|
void set_audio_streams_seen(std::uint32_t count)
|
||||||
{
|
{
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr) {
|
||||||
{
|
|
||||||
block_->status.audio_streams_seen = count;
|
block_->status.audio_streams_seen = count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,8 +167,7 @@ public:
|
|||||||
// Publish a tracked stream's format/role into its debug slot.
|
// Publish a tracked stream's format/role into its debug slot.
|
||||||
void publish_audio_stream(std::uint32_t slot, const AudioStreamInfo& info)
|
void publish_audio_stream(std::uint32_t slot, const AudioStreamInfo& info)
|
||||||
{
|
{
|
||||||
if (block_ != nullptr && slot < kMaxAudioStreams)
|
if (block_ != nullptr && slot < kMaxAudioStreams) {
|
||||||
{
|
|
||||||
block_->status.audio_streams[slot] = info;
|
block_->status.audio_streams[slot] = info;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,12 +175,12 @@ public:
|
|||||||
// Update a tracked stream's cumulative frame count (host derives live/idle).
|
// Update a tracked stream's cumulative frame count (host derives live/idle).
|
||||||
void note_audio_frames(std::uint32_t slot, std::uint64_t frames)
|
void note_audio_frames(std::uint32_t slot, std::uint64_t frames)
|
||||||
{
|
{
|
||||||
if (block_ != nullptr && slot < kMaxAudioStreams)
|
if (block_ != nullptr && slot < kMaxAudioStreams) {
|
||||||
{
|
|
||||||
// atomic_ref so the host's cross-process read isn't torn (notably an x86 DLL -> x64 host,
|
// atomic_ref so the host's cross-process read isn't torn (notably an x86 DLL -> x64 host,
|
||||||
// where a plain 64-bit store is two halves). The field stays plain POD so AudioStreamInfo
|
// where a plain 64-bit store is two halves). The field stays plain POD so AudioStreamInfo
|
||||||
// remains trivially copyable for the wholesale publishes elsewhere.
|
// remains trivially copyable for the wholesale publishes elsewhere.
|
||||||
std::atomic_ref(block_->status.audio_streams[slot].frames_rendered).store(frames, std::memory_order_relaxed);
|
std::atomic_ref(block_->status.audio_streams[slot].frames_rendered)
|
||||||
|
.store(frames, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +189,7 @@ public:
|
|||||||
// Record that the game's Present() ran (diagnostic counter, hook is sole writer).
|
// Record that the game's Present() ran (diagnostic counter, hook is sole writer).
|
||||||
void note_present()
|
void note_present()
|
||||||
{
|
{
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr) {
|
||||||
{
|
|
||||||
std::atomic_ref(block_->video.present_calls).fetch_add(1, std::memory_order_relaxed);
|
std::atomic_ref(block_->video.present_calls).fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,8 +198,7 @@ public:
|
|||||||
// keyed mutex was held by the host (we skip rather than block the game's render thread).
|
// keyed mutex was held by the host (we skip rather than block the game's render thread).
|
||||||
void note_video_dropped()
|
void note_video_dropped()
|
||||||
{
|
{
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr) {
|
||||||
{
|
|
||||||
std::atomic_ref(block_->video.frames_dropped).fetch_add(1, std::memory_order_relaxed);
|
std::atomic_ref(block_->video.frames_dropped).fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,8 +208,7 @@ public:
|
|||||||
// polls. The texture itself is shared out-of-band by name, not through here.
|
// polls. The texture itself is shared out-of-band by name, not through here.
|
||||||
void publish_video_frame(std::uint32_t width, std::uint32_t height, std::uint32_t format)
|
void publish_video_frame(std::uint32_t width, std::uint32_t height, std::uint32_t format)
|
||||||
{
|
{
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr) {
|
||||||
{
|
|
||||||
block_->video.width = width;
|
block_->video.width = width;
|
||||||
block_->video.height = height;
|
block_->video.height = height;
|
||||||
block_->video.format = format;
|
block_->video.format = format;
|
||||||
@@ -249,26 +223,20 @@ public:
|
|||||||
|
|
||||||
// The host's MKB event queue (nullptr if not connected). The MKB subsystem
|
// The host's MKB event queue (nullptr if not connected). The MKB subsystem
|
||||||
// drains it; the host is the sole producer.
|
// drains it; the host is the sole producer.
|
||||||
[[nodiscard]] MkbRing* mkb_ring()
|
[[nodiscard]] MkbRing* mkb_ring() { return block_ != nullptr ? &block_->mkb : nullptr; }
|
||||||
{
|
|
||||||
return block_ != nullptr ? &block_->mkb : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Hook registry -----------------------------------------------------
|
// --- Hook registry -----------------------------------------------------
|
||||||
|
|
||||||
// Publish the installed-hooks table (name / subsystem / installed / calls).
|
// Publish the installed-hooks table (name / subsystem / installed / calls).
|
||||||
void publish_hook_entries(const HookEntry* entries, std::uint32_t count)
|
void publish_hook_entries(const HookEntry* entries, std::uint32_t count)
|
||||||
{
|
{
|
||||||
if (block_ == nullptr)
|
if (block_ == nullptr) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (count > kMaxHookEntries)
|
if (count > kMaxHookEntries) {
|
||||||
{
|
|
||||||
count = kMaxHookEntries;
|
count = kMaxHookEntries;
|
||||||
}
|
}
|
||||||
for (std::uint32_t i = 0; i < count; ++i)
|
for (std::uint32_t i = 0; i < count; ++i) {
|
||||||
{
|
|
||||||
block_->status.hook_entries[i] = entries[i];
|
block_->status.hook_entries[i] = entries[i];
|
||||||
}
|
}
|
||||||
block_->status.hook_entry_count = count;
|
block_->status.hook_entry_count = count;
|
||||||
|
|||||||
@@ -15,11 +15,9 @@
|
|||||||
#include "hook_registry.hpp"
|
#include "hook_registry.hpp"
|
||||||
#include "vtable_hook.hpp"
|
#include "vtable_hook.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
DetourGate g_gate; // drains in-flight polling detours before remove tears the hooks down
|
DetourGate g_gate; // drains in-flight polling detours before remove tears the hooks down
|
||||||
|
|
||||||
@@ -82,9 +80,8 @@ SHORT WINAPI hk_GetAsyncKeyState(int vkey)
|
|||||||
{
|
{
|
||||||
DetourGate::Guard guard(g_gate);
|
DetourGate::Guard guard(g_gate);
|
||||||
const SHORT orig = g_hk_async.stdcall<SHORT>(vkey);
|
const SHORT orig = g_hk_async.stdcall<SHORT>(vkey);
|
||||||
if (g_active.load(std::memory_order_relaxed) && vkey >= 0 && vkey < 256 &&
|
if (g_active.load(std::memory_order_relaxed) && vkey >= 0 && vkey < 256
|
||||||
g_key_down[vkey].load(std::memory_order_relaxed))
|
&& g_key_down[vkey].load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
return static_cast<SHORT>(0x8000) | (orig & 0x1);
|
return static_cast<SHORT>(0x8000) | (orig & 0x1);
|
||||||
}
|
}
|
||||||
return orig;
|
return orig;
|
||||||
@@ -94,12 +91,9 @@ BOOL WINAPI hk_GetKeyboardState(PBYTE state)
|
|||||||
{
|
{
|
||||||
DetourGate::Guard guard(g_gate);
|
DetourGate::Guard guard(g_gate);
|
||||||
const BOOL r = g_hk_kbstate.stdcall<BOOL>(state);
|
const BOOL r = g_hk_kbstate.stdcall<BOOL>(state);
|
||||||
if (r && state != nullptr && g_active.load(std::memory_order_relaxed))
|
if (r && state != nullptr && g_active.load(std::memory_order_relaxed)) {
|
||||||
{
|
for (int vk = 0; vk < 256; ++vk) {
|
||||||
for (int vk = 0; vk < 256; ++vk)
|
if (g_key_down[vk].load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
if (g_key_down[vk].load(std::memory_order_relaxed))
|
|
||||||
{
|
|
||||||
state[vk] |= 0x80;
|
state[vk] |= 0x80;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,11 +105,9 @@ BOOL WINAPI hk_GetCursorPos(LPPOINT pt)
|
|||||||
{
|
{
|
||||||
DetourGate::Guard guard(g_gate);
|
DetourGate::Guard guard(g_gate);
|
||||||
const BOOL r = g_hk_cursor.stdcall<BOOL>(pt);
|
const BOOL r = g_hk_cursor.stdcall<BOOL>(pt);
|
||||||
if (g_active.load(std::memory_order_relaxed) && g_have_cursor.load(std::memory_order_relaxed) && pt != nullptr)
|
if (g_active.load(std::memory_order_relaxed) && g_have_cursor.load(std::memory_order_relaxed) && pt != nullptr) {
|
||||||
{
|
|
||||||
auto* hwnd = static_cast<HWND>(g_target.load(std::memory_order_relaxed));
|
auto* hwnd = static_cast<HWND>(g_target.load(std::memory_order_relaxed));
|
||||||
if (hwnd != nullptr)
|
if (hwnd != nullptr) {
|
||||||
{
|
|
||||||
POINT c{g_cursor_x.load(std::memory_order_relaxed), g_cursor_y.load(std::memory_order_relaxed)};
|
POINT c{g_cursor_x.load(std::memory_order_relaxed), g_cursor_y.load(std::memory_order_relaxed)};
|
||||||
ClientToScreen(hwnd, &c); // synth state is game-client; GetCursorPos is screen-space
|
ClientToScreen(hwnd, &c); // synth state is game-client; GetCursorPos is screen-space
|
||||||
*pt = c;
|
*pt = c;
|
||||||
@@ -140,31 +132,25 @@ HRESULT STDMETHODCALLTYPE hk_DI_GetDeviceState(IDirectInputDevice8W* self, DWORD
|
|||||||
{
|
{
|
||||||
DetourGate::Guard guard(g_gate);
|
DetourGate::Guard guard(g_gate);
|
||||||
const HRESULT hr = g_vh_di_getstate.original<DI_GetDeviceStateFn>()(self, cb, data);
|
const HRESULT hr = g_vh_di_getstate.original<DI_GetDeviceStateFn>()(self, cb, data);
|
||||||
if (FAILED(hr) || data == nullptr || !g_active.load(std::memory_order_relaxed))
|
if (FAILED(hr) || data == nullptr || !g_active.load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
return hr;
|
return hr;
|
||||||
}
|
}
|
||||||
hook_note_call(g_id_di_getstate);
|
hook_note_call(g_id_di_getstate);
|
||||||
if (cb == 256) // keyboard: BYTE[256] indexed by DIK (scan code); high bit = pressed
|
if (cb == 256) // keyboard: BYTE[256] indexed by DIK (scan code); high bit = pressed
|
||||||
{
|
{
|
||||||
BYTE* keys = static_cast<BYTE*>(data);
|
BYTE* keys = static_cast<BYTE*>(data);
|
||||||
for (int vk = 0; vk < 256; ++vk)
|
for (int vk = 0; vk < 256; ++vk) {
|
||||||
{
|
if (g_key_down[vk].load(std::memory_order_relaxed)) {
|
||||||
if (g_key_down[vk].load(std::memory_order_relaxed))
|
|
||||||
{
|
|
||||||
const BYTE dik = vk_to_dik(vk);
|
const BYTE dik = vk_to_dik(vk);
|
||||||
if (dik != 0)
|
if (dik != 0) {
|
||||||
{
|
|
||||||
keys[dik] |= 0x80;
|
keys[dik] |= 0x80;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if (cb == sizeof(DIMOUSESTATE) || cb == sizeof(DIMOUSESTATE2)) // mouse (DIMOUSESTATE2 is a superset)
|
||||||
else if (cb == sizeof(DIMOUSESTATE) || cb == sizeof(DIMOUSESTATE2)) // mouse (DIMOUSESTATE2 is a superset)
|
|
||||||
{
|
{
|
||||||
auto* m = static_cast<DIMOUSESTATE*>(data); // the shared lead fields (lX/lY/lZ/rgbButtons)
|
auto* m = static_cast<DIMOUSESTATE*>(data); // the shared lead fields (lX/lY/lZ/rgbButtons)
|
||||||
if (g_have_cursor.load(std::memory_order_relaxed))
|
if (g_have_cursor.load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
const long x = g_cursor_x.load(std::memory_order_relaxed);
|
const long x = g_cursor_x.load(std::memory_order_relaxed);
|
||||||
const long y = g_cursor_y.load(std::memory_order_relaxed);
|
const long y = g_cursor_y.load(std::memory_order_relaxed);
|
||||||
if (g_di_mouse_primed.load(std::memory_order_relaxed)) // relative delta from our cursor
|
if (g_di_mouse_primed.load(std::memory_order_relaxed)) // relative delta from our cursor
|
||||||
@@ -176,16 +162,13 @@ HRESULT STDMETHODCALLTYPE hk_DI_GetDeviceState(IDirectInputDevice8W* self, DWORD
|
|||||||
g_di_mouse_last_y.store(y, std::memory_order_relaxed);
|
g_di_mouse_last_y.store(y, std::memory_order_relaxed);
|
||||||
g_di_mouse_primed.store(true, std::memory_order_relaxed);
|
g_di_mouse_primed.store(true, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
if (g_key_down[VK_LBUTTON].load(std::memory_order_relaxed))
|
if (g_key_down[VK_LBUTTON].load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
m->rgbButtons[0] |= 0x80;
|
m->rgbButtons[0] |= 0x80;
|
||||||
}
|
}
|
||||||
if (g_key_down[VK_RBUTTON].load(std::memory_order_relaxed))
|
if (g_key_down[VK_RBUTTON].load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
m->rgbButtons[1] |= 0x80;
|
m->rgbButtons[1] |= 0x80;
|
||||||
}
|
}
|
||||||
if (g_key_down[VK_MBUTTON].load(std::memory_order_relaxed))
|
if (g_key_down[VK_MBUTTON].load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
m->rgbButtons[2] |= 0x80;
|
m->rgbButtons[2] |= 0x80;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,38 +187,31 @@ bool is_our_raw(HRAWINPUT h)
|
|||||||
UINT WINAPI hk_GetRawInputData(HRAWINPUT hri, UINT cmd, LPVOID pData, PUINT pcbSize, UINT cbHeader)
|
UINT WINAPI hk_GetRawInputData(HRAWINPUT hri, UINT cmd, LPVOID pData, PUINT pcbSize, UINT cbHeader)
|
||||||
{
|
{
|
||||||
DetourGate::Guard guard(g_gate);
|
DetourGate::Guard guard(g_gate);
|
||||||
if (g_active.load(std::memory_order_relaxed) && is_our_raw(hri))
|
if (g_active.load(std::memory_order_relaxed) && is_our_raw(hri)) {
|
||||||
{
|
|
||||||
hook_note_call(g_id_rawinput);
|
hook_note_call(g_id_rawinput);
|
||||||
const RAWINPUT* ri = reinterpret_cast<const RAWINPUT*>(hri);
|
const RAWINPUT* ri = reinterpret_cast<const RAWINPUT*>(hri);
|
||||||
const UINT body = ri->header.dwType == RIM_TYPEMOUSE ? sizeof(RAWMOUSE) : sizeof(RAWKEYBOARD);
|
const UINT body = ri->header.dwType == RIM_TYPEMOUSE ? sizeof(RAWMOUSE) : sizeof(RAWKEYBOARD);
|
||||||
const UINT full = sizeof(RAWINPUTHEADER) + body;
|
const UINT full = sizeof(RAWINPUTHEADER) + body;
|
||||||
if (pcbSize == nullptr)
|
if (pcbSize == nullptr) {
|
||||||
{
|
|
||||||
return static_cast<UINT>(-1);
|
return static_cast<UINT>(-1);
|
||||||
}
|
}
|
||||||
if (cmd == RID_HEADER)
|
if (cmd == RID_HEADER) {
|
||||||
{
|
if (pData == nullptr) {
|
||||||
if (pData == nullptr)
|
|
||||||
{
|
|
||||||
*pcbSize = sizeof(RAWINPUTHEADER);
|
*pcbSize = sizeof(RAWINPUTHEADER);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (*pcbSize < sizeof(RAWINPUTHEADER))
|
if (*pcbSize < sizeof(RAWINPUTHEADER)) {
|
||||||
{
|
|
||||||
return static_cast<UINT>(-1);
|
return static_cast<UINT>(-1);
|
||||||
}
|
}
|
||||||
memcpy(pData, &ri->header, sizeof(RAWINPUTHEADER));
|
memcpy(pData, &ri->header, sizeof(RAWINPUTHEADER));
|
||||||
return sizeof(RAWINPUTHEADER);
|
return sizeof(RAWINPUTHEADER);
|
||||||
}
|
}
|
||||||
// RID_INPUT: the full header + body.
|
// RID_INPUT: the full header + body.
|
||||||
if (pData == nullptr)
|
if (pData == nullptr) {
|
||||||
{
|
|
||||||
*pcbSize = full;
|
*pcbSize = full;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (*pcbSize < full)
|
if (*pcbSize < full) {
|
||||||
{
|
|
||||||
return static_cast<UINT>(-1);
|
return static_cast<UINT>(-1);
|
||||||
}
|
}
|
||||||
memcpy(pData, ri, full);
|
memcpy(pData, ri, full);
|
||||||
@@ -247,8 +223,7 @@ UINT WINAPI hk_GetRawInputData(HRAWINPUT hri, UINT cmd, LPVOID pData, PUINT pcbS
|
|||||||
// Post a synthetic Raw Input event to `hwnd` (a WM_INPUT carrying one of our g_raw_slots).
|
// Post a synthetic Raw Input event to `hwnd` (a WM_INPUT carrying one of our g_raw_slots).
|
||||||
void post_raw_key(HWND hwnd, UINT vk, bool down)
|
void post_raw_key(HWND hwnd, UINT vk, bool down)
|
||||||
{
|
{
|
||||||
if (hwnd == nullptr || !g_hk_getrawinputdata)
|
if (hwnd == nullptr || !g_hk_getrawinputdata) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
RAWINPUT& ri = g_raw_slots[g_raw_head.fetch_add(1, std::memory_order_relaxed) % kRawSlots];
|
RAWINPUT& ri = g_raw_slots[g_raw_head.fetch_add(1, std::memory_order_relaxed) % kRawSlots];
|
||||||
@@ -264,8 +239,7 @@ void post_raw_key(HWND hwnd, UINT vk, bool down)
|
|||||||
|
|
||||||
void post_raw_mouse(HWND hwnd, USHORT button_flags)
|
void post_raw_mouse(HWND hwnd, USHORT button_flags)
|
||||||
{
|
{
|
||||||
if (hwnd == nullptr || !g_hk_getrawinputdata)
|
if (hwnd == nullptr || !g_hk_getrawinputdata) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
RAWINPUT& ri = g_raw_slots[g_raw_head.fetch_add(1, std::memory_order_relaxed) % kRawSlots];
|
RAWINPUT& ri = g_raw_slots[g_raw_head.fetch_add(1, std::memory_order_relaxed) % kRawSlots];
|
||||||
@@ -283,8 +257,7 @@ LPARAM key_lparam(UINT vk, bool key_up)
|
|||||||
{
|
{
|
||||||
const UINT scan = MapVirtualKeyW(vk, MAPVK_VK_TO_VSC);
|
const UINT scan = MapVirtualKeyW(vk, MAPVK_VK_TO_VSC);
|
||||||
LPARAM lp = 1 | (static_cast<LPARAM>(scan) << 16); // repeat count 1 + scan code
|
LPARAM lp = 1 | (static_cast<LPARAM>(scan) << 16); // repeat count 1 + scan code
|
||||||
if (key_up)
|
if (key_up) {
|
||||||
{
|
|
||||||
lp |= (LPARAM{1} << 30) | (LPARAM{1} << 31); // previous-down + transition (key released)
|
lp |= (LPARAM{1} << 30) | (LPARAM{1} << 31); // previous-down + transition (key released)
|
||||||
}
|
}
|
||||||
return lp;
|
return lp;
|
||||||
@@ -292,8 +265,7 @@ LPARAM key_lparam(UINT vk, bool key_up)
|
|||||||
|
|
||||||
void set_key(UINT vk, bool down)
|
void set_key(UINT vk, bool down)
|
||||||
{
|
{
|
||||||
if (vk < 256)
|
if (vk < 256) {
|
||||||
{
|
|
||||||
g_key_down[vk].store(down, std::memory_order_relaxed);
|
g_key_down[vk].store(down, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,16 +273,13 @@ void set_key(UINT vk, bool down)
|
|||||||
WPARAM mouse_button_wparam()
|
WPARAM mouse_button_wparam()
|
||||||
{
|
{
|
||||||
WPARAM w = 0;
|
WPARAM w = 0;
|
||||||
if (g_key_down[VK_LBUTTON].load(std::memory_order_relaxed))
|
if (g_key_down[VK_LBUTTON].load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
w |= MK_LBUTTON;
|
w |= MK_LBUTTON;
|
||||||
}
|
}
|
||||||
if (g_key_down[VK_RBUTTON].load(std::memory_order_relaxed))
|
if (g_key_down[VK_RBUTTON].load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
w |= MK_RBUTTON;
|
w |= MK_RBUTTON;
|
||||||
}
|
}
|
||||||
if (g_key_down[VK_MBUTTON].load(std::memory_order_relaxed))
|
if (g_key_down[VK_MBUTTON].load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
w |= MK_MBUTTON;
|
w |= MK_MBUTTON;
|
||||||
}
|
}
|
||||||
return w;
|
return w;
|
||||||
@@ -324,36 +293,25 @@ void handle_mouse(const MkbEvent& ev, bool down, HWND hwnd)
|
|||||||
|
|
||||||
const UINT vk = ev.code == 0 ? VK_LBUTTON : ev.code == 1 ? VK_RBUTTON : VK_MBUTTON;
|
const UINT vk = ev.code == 0 ? VK_LBUTTON : ev.code == 1 ? VK_RBUTTON : VK_MBUTTON;
|
||||||
set_key(vk, down);
|
set_key(vk, down);
|
||||||
if (hwnd == nullptr)
|
if (hwnd == nullptr) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UINT msg;
|
UINT msg;
|
||||||
if (ev.code == 0)
|
if (ev.code == 0) {
|
||||||
{
|
|
||||||
msg = down ? WM_LBUTTONDOWN : WM_LBUTTONUP;
|
msg = down ? WM_LBUTTONDOWN : WM_LBUTTONUP;
|
||||||
}
|
} else if (ev.code == 1) {
|
||||||
else if (ev.code == 1)
|
|
||||||
{
|
|
||||||
msg = down ? WM_RBUTTONDOWN : WM_RBUTTONUP;
|
msg = down ? WM_RBUTTONDOWN : WM_RBUTTONUP;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
msg = down ? WM_MBUTTONDOWN : WM_MBUTTONUP;
|
msg = down ? WM_MBUTTONDOWN : WM_MBUTTONUP;
|
||||||
}
|
}
|
||||||
PostMessageW(hwnd, msg, mouse_button_wparam(), MAKELPARAM(ev.x, ev.y));
|
PostMessageW(hwnd, msg, mouse_button_wparam(), MAKELPARAM(ev.x, ev.y));
|
||||||
// Also feed Raw Input games (button event; relative move isn't in the MKB event stream).
|
// Also feed Raw Input games (button event; relative move isn't in the MKB event stream).
|
||||||
USHORT rflags = 0;
|
USHORT rflags = 0;
|
||||||
if (ev.code == 0)
|
if (ev.code == 0) {
|
||||||
{
|
|
||||||
rflags = down ? RI_MOUSE_LEFT_BUTTON_DOWN : RI_MOUSE_LEFT_BUTTON_UP;
|
rflags = down ? RI_MOUSE_LEFT_BUTTON_DOWN : RI_MOUSE_LEFT_BUTTON_UP;
|
||||||
}
|
} else if (ev.code == 1) {
|
||||||
else if (ev.code == 1)
|
|
||||||
{
|
|
||||||
rflags = down ? RI_MOUSE_RIGHT_BUTTON_DOWN : RI_MOUSE_RIGHT_BUTTON_UP;
|
rflags = down ? RI_MOUSE_RIGHT_BUTTON_DOWN : RI_MOUSE_RIGHT_BUTTON_UP;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
rflags = down ? RI_MOUSE_MIDDLE_BUTTON_DOWN : RI_MOUSE_MIDDLE_BUTTON_UP;
|
rflags = down ? RI_MOUSE_MIDDLE_BUTTON_DOWN : RI_MOUSE_MIDDLE_BUTTON_UP;
|
||||||
}
|
}
|
||||||
post_raw_mouse(hwnd, rflags);
|
post_raw_mouse(hwnd, rflags);
|
||||||
@@ -364,8 +322,7 @@ void handle_wheel(const MkbEvent& ev, HWND hwnd)
|
|||||||
g_cursor_x.store(ev.x, std::memory_order_relaxed);
|
g_cursor_x.store(ev.x, std::memory_order_relaxed);
|
||||||
g_cursor_y.store(ev.y, std::memory_order_relaxed);
|
g_cursor_y.store(ev.y, std::memory_order_relaxed);
|
||||||
g_have_cursor.store(true, std::memory_order_relaxed);
|
g_have_cursor.store(true, std::memory_order_relaxed);
|
||||||
if (hwnd == nullptr)
|
if (hwnd == nullptr) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
POINT pt{ev.x, ev.y};
|
POINT pt{ev.x, ev.y};
|
||||||
@@ -376,15 +333,12 @@ void handle_wheel(const MkbEvent& ev, HWND hwnd)
|
|||||||
|
|
||||||
void install_user32_hook(HMODULE user32, const char* name, void* detour, safetyhook::InlineHook& slot, int id)
|
void install_user32_hook(HMODULE user32, const char* name, void* detour, safetyhook::InlineHook& slot, int id)
|
||||||
{
|
{
|
||||||
if (user32 == nullptr)
|
if (user32 == nullptr) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (void* target = reinterpret_cast<void*>(GetProcAddress(user32, name)))
|
if (void* target = reinterpret_cast<void*>(GetProcAddress(user32, name))) {
|
||||||
{
|
|
||||||
install_inline(slot, target, detour); // StartDisabled -> assign -> enable (no install race)
|
install_inline(slot, target, detour); // StartDisabled -> assign -> enable (no install race)
|
||||||
if (slot)
|
if (slot) {
|
||||||
{
|
|
||||||
hook_set_installed(id, true);
|
hook_set_installed(id, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -396,35 +350,27 @@ void install_user32_hook(HMODULE user32, const char* name, void* detour, safetyh
|
|||||||
// on the calling thread (the worker thread is).
|
// on the calling thread (the worker thread is).
|
||||||
bool install_dinput_hook()
|
bool install_dinput_hook()
|
||||||
{
|
{
|
||||||
if (g_vh_di_getstate)
|
if (g_vh_di_getstate) {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
HMODULE di = GetModuleHandleW(L"dinput8.dll");
|
HMODULE di = GetModuleHandleW(L"dinput8.dll");
|
||||||
if (di == nullptr)
|
if (di == nullptr) {
|
||||||
{
|
|
||||||
return false; // not a DirectInput game (yet)
|
return false; // not a DirectInput game (yet)
|
||||||
}
|
}
|
||||||
using PFN_DI8Create = HRESULT(WINAPI*)(HINSTANCE, DWORD, REFIID, LPVOID*, LPUNKNOWN);
|
using PFN_DI8Create = HRESULT(WINAPI*)(HINSTANCE, DWORD, REFIID, LPVOID*, LPUNKNOWN);
|
||||||
auto create = reinterpret_cast<PFN_DI8Create>(GetProcAddress(di, "DirectInput8Create"));
|
auto create = reinterpret_cast<PFN_DI8Create>(GetProcAddress(di, "DirectInput8Create"));
|
||||||
if (create == nullptr)
|
if (create == nullptr) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (g_di_probe == nullptr)
|
if (g_di_probe == nullptr) {
|
||||||
{
|
|
||||||
if (FAILED(create(GetModuleHandleW(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8W,
|
if (FAILED(create(GetModuleHandleW(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8W,
|
||||||
reinterpret_cast<void**>(&g_di_probe), nullptr)) ||
|
reinterpret_cast<void**>(&g_di_probe), nullptr))
|
||||||
g_di_probe == nullptr)
|
|| g_di_probe == nullptr) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (g_di_probe_kbd == nullptr)
|
if (g_di_probe_kbd == nullptr) {
|
||||||
{
|
if (FAILED(g_di_probe->CreateDevice(GUID_SysKeyboard, &g_di_probe_kbd, nullptr)) || g_di_probe_kbd == nullptr) {
|
||||||
if (FAILED(g_di_probe->CreateDevice(GUID_SysKeyboard, &g_di_probe_kbd, nullptr)) ||
|
|
||||||
g_di_probe_kbd == nullptr)
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -438,13 +384,11 @@ bool install_dinput_hook()
|
|||||||
|
|
||||||
bool install_mkb_hooks(IpcClient& ipc)
|
bool install_mkb_hooks(IpcClient& ipc)
|
||||||
{
|
{
|
||||||
if (g_installed)
|
if (g_installed) {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g_id_pump < 0)
|
if (g_id_pump < 0) {
|
||||||
{
|
|
||||||
g_id_pump = hook_register("MKB pump (PostMessage)", HookSubsys_Mkb);
|
g_id_pump = hook_register("MKB pump (PostMessage)", HookSubsys_Mkb);
|
||||||
g_id_async = hook_register("GetAsyncKeyState", HookSubsys_Mkb);
|
g_id_async = hook_register("GetAsyncKeyState", HookSubsys_Mkb);
|
||||||
g_id_kbstate = hook_register("GetKeyboardState", HookSubsys_Mkb);
|
g_id_kbstate = hook_register("GetKeyboardState", HookSubsys_Mkb);
|
||||||
@@ -454,8 +398,7 @@ bool install_mkb_hooks(IpcClient& ipc)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fresh synthesized state so a previous session leaves no stuck keys.
|
// Fresh synthesized state so a previous session leaves no stuck keys.
|
||||||
for (int vk = 0; vk < 256; ++vk)
|
for (int vk = 0; vk < 256; ++vk) {
|
||||||
{
|
|
||||||
g_key_down[vk].store(false, std::memory_order_relaxed);
|
g_key_down[vk].store(false, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
g_have_cursor.store(false, std::memory_order_relaxed);
|
g_have_cursor.store(false, std::memory_order_relaxed);
|
||||||
@@ -469,8 +412,8 @@ bool install_mkb_hooks(IpcClient& ipc)
|
|||||||
install_user32_hook(user32, "GetCursorPos", reinterpret_cast<void*>(&hk_GetCursorPos), g_hk_cursor, g_id_cursor);
|
install_user32_hook(user32, "GetCursorPos", reinterpret_cast<void*>(&hk_GetCursorPos), g_hk_cursor, g_id_cursor);
|
||||||
// Raw Input: synthesize WM_INPUT (in mkb_pump) + serve it from this hook, for games that read
|
// Raw Input: synthesize WM_INPUT (in mkb_pump) + serve it from this hook, for games that read
|
||||||
// keyboard/mouse via GetRawInputData. GetRawInputData has a clean prologue -> inline hook is OK.
|
// keyboard/mouse via GetRawInputData. GetRawInputData has a clean prologue -> inline hook is OK.
|
||||||
install_user32_hook(user32, "GetRawInputData", reinterpret_cast<void*>(&hk_GetRawInputData),
|
install_user32_hook(user32, "GetRawInputData", reinterpret_cast<void*>(&hk_GetRawInputData), g_hk_getrawinputdata,
|
||||||
g_hk_getrawinputdata, g_id_rawinput);
|
g_id_rawinput);
|
||||||
// DirectInput: vtable-swap GetDeviceState (best-effort -- dinput8.dll may load later, retried
|
// DirectInput: vtable-swap GetDeviceState (best-effort -- dinput8.dll may load later, retried
|
||||||
// from mkb_pump). The probe is built once and kept alive (avoids COM churn on a re-enable).
|
// from mkb_pump). The probe is built once and kept alive (avoids COM churn on a re-enable).
|
||||||
g_di_mouse_primed.store(false, std::memory_order_relaxed);
|
g_di_mouse_primed.store(false, std::memory_order_relaxed);
|
||||||
@@ -485,8 +428,7 @@ bool install_mkb_hooks(IpcClient& ipc)
|
|||||||
|
|
||||||
void remove_mkb_hooks()
|
void remove_mkb_hooks()
|
||||||
{
|
{
|
||||||
if (!g_installed)
|
if (!g_installed) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
g_active.store(false, std::memory_order_release);
|
g_active.store(false, std::memory_order_release);
|
||||||
@@ -503,8 +445,7 @@ void remove_mkb_hooks()
|
|||||||
g_gate.drain(); // wait for any in-flight polling / DI / raw detour before clearing state
|
g_gate.drain(); // wait for any in-flight polling / DI / raw detour before clearing state
|
||||||
hook_set_installed(g_id_di_getstate, false);
|
hook_set_installed(g_id_di_getstate, false);
|
||||||
hook_set_installed(g_id_rawinput, false);
|
hook_set_installed(g_id_rawinput, false);
|
||||||
for (int vk = 0; vk < 256; ++vk)
|
for (int vk = 0; vk < 256; ++vk) {
|
||||||
{
|
|
||||||
g_key_down[vk].store(false, std::memory_order_relaxed); // no stuck keys
|
g_key_down[vk].store(false, std::memory_order_relaxed); // no stuck keys
|
||||||
}
|
}
|
||||||
g_have_cursor.store(false, std::memory_order_relaxed);
|
g_have_cursor.store(false, std::memory_order_relaxed);
|
||||||
@@ -518,47 +459,39 @@ void remove_mkb_hooks()
|
|||||||
void mkb_pump(IpcClient& ipc)
|
void mkb_pump(IpcClient& ipc)
|
||||||
{
|
{
|
||||||
MkbRing* ring = ipc.mkb_ring();
|
MkbRing* ring = ipc.mkb_ring();
|
||||||
if (ring == nullptr || !g_active.load(std::memory_order_relaxed))
|
if (ring == nullptr || !g_active.load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!g_vh_di_getstate)
|
if (!g_vh_di_getstate) {
|
||||||
{
|
|
||||||
install_dinput_hook(); // dinput8.dll can load after we installed; keep retrying cheaply
|
install_dinput_hook(); // dinput8.dll can load after we installed; keep retrying cheaply
|
||||||
}
|
}
|
||||||
|
|
||||||
HWND hwnd = static_cast<HWND>(g_target.load(std::memory_order_relaxed));
|
HWND hwnd = static_cast<HWND>(g_target.load(std::memory_order_relaxed));
|
||||||
if (hwnd == nullptr || !IsWindow(hwnd))
|
if (hwnd == nullptr || !IsWindow(hwnd)) {
|
||||||
{
|
|
||||||
hwnd = find_main_window(GetCurrentProcessId());
|
hwnd = find_main_window(GetCurrentProcessId());
|
||||||
g_target.store(hwnd, std::memory_order_relaxed);
|
g_target.store(hwnd, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
MkbEvent ev{};
|
MkbEvent ev{};
|
||||||
while (pop_mkb_event(*ring, ev))
|
while (pop_mkb_event(*ring, ev)) {
|
||||||
{
|
|
||||||
hook_note_call(g_id_pump);
|
hook_note_call(g_id_pump);
|
||||||
switch (ev.type)
|
switch (ev.type) {
|
||||||
{
|
|
||||||
case Mkb_KeyDown:
|
case Mkb_KeyDown:
|
||||||
set_key(ev.code, true);
|
set_key(ev.code, true);
|
||||||
if (hwnd != nullptr)
|
if (hwnd != nullptr) {
|
||||||
{
|
|
||||||
PostMessageW(hwnd, WM_KEYDOWN, ev.code, key_lparam(ev.code, false));
|
PostMessageW(hwnd, WM_KEYDOWN, ev.code, key_lparam(ev.code, false));
|
||||||
}
|
}
|
||||||
post_raw_key(hwnd, ev.code, true); // also feed Raw Input games
|
post_raw_key(hwnd, ev.code, true); // also feed Raw Input games
|
||||||
break;
|
break;
|
||||||
case Mkb_KeyUp:
|
case Mkb_KeyUp:
|
||||||
set_key(ev.code, false);
|
set_key(ev.code, false);
|
||||||
if (hwnd != nullptr)
|
if (hwnd != nullptr) {
|
||||||
{
|
|
||||||
PostMessageW(hwnd, WM_KEYUP, ev.code, key_lparam(ev.code, true));
|
PostMessageW(hwnd, WM_KEYUP, ev.code, key_lparam(ev.code, true));
|
||||||
}
|
}
|
||||||
post_raw_key(hwnd, ev.code, false);
|
post_raw_key(hwnd, ev.code, false);
|
||||||
break;
|
break;
|
||||||
case Mkb_Char:
|
case Mkb_Char:
|
||||||
if (hwnd != nullptr)
|
if (hwnd != nullptr) {
|
||||||
{
|
|
||||||
PostMessageW(hwnd, WM_CHAR, ev.code, 1);
|
PostMessageW(hwnd, WM_CHAR, ev.code, 1);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -10,8 +10,7 @@
|
|||||||
|
|
||||||
#include "ipc_client.hpp"
|
#include "ipc_client.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
bool install_mkb_hooks(IpcClient& ipc);
|
bool install_mkb_hooks(IpcClient& ipc);
|
||||||
void remove_mkb_hooks();
|
void remove_mkb_hooks();
|
||||||
|
|||||||
@@ -17,11 +17,9 @@
|
|||||||
#include "hook_registry.hpp"
|
#include "hook_registry.hpp"
|
||||||
#include "shared_video_texture.hpp"
|
#include "shared_video_texture.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
DetourGate g_gate; // drains in-flight swap detours before remove frees the shared D3D state
|
DetourGate g_gate; // drains in-flight swap detours before remove frees the shared D3D state
|
||||||
|
|
||||||
@@ -65,13 +63,11 @@ thread_local bool t_in_swap = false;
|
|||||||
|
|
||||||
void resolve_gl()
|
void resolve_gl()
|
||||||
{
|
{
|
||||||
if (g_gl_resolved)
|
if (g_gl_resolved) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
HMODULE gl = GetModuleHandleW(L"opengl32.dll");
|
HMODULE gl = GetModuleHandleW(L"opengl32.dll");
|
||||||
if (gl == nullptr)
|
if (gl == nullptr) {
|
||||||
{
|
|
||||||
return; // not an OpenGL process (yet)
|
return; // not an OpenGL process (yet)
|
||||||
}
|
}
|
||||||
g_glReadPixels = reinterpret_cast<PFN_glReadPixels>(GetProcAddress(gl, "glReadPixels"));
|
g_glReadPixels = reinterpret_cast<PFN_glReadPixels>(GetProcAddress(gl, "glReadPixels"));
|
||||||
@@ -82,14 +78,12 @@ void resolve_gl()
|
|||||||
|
|
||||||
bool ensure_device()
|
bool ensure_device()
|
||||||
{
|
{
|
||||||
if (g_device != nullptr)
|
if (g_device != nullptr) {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0,
|
const HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION,
|
||||||
D3D11_SDK_VERSION, &g_device, nullptr, &g_ctx);
|
&g_device, nullptr, &g_ctx);
|
||||||
if (FAILED(hr) || g_device == nullptr)
|
if (FAILED(hr) || g_device == nullptr) {
|
||||||
{
|
|
||||||
logf("opengl: D3D11CreateDevice failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
logf("opengl: D3D11CreateDevice failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -100,10 +94,8 @@ bool ensure_device()
|
|||||||
void capture_gl(HDC hdc)
|
void capture_gl(HDC hdc)
|
||||||
{
|
{
|
||||||
resolve_gl();
|
resolve_gl();
|
||||||
if (!g_gl_resolved || g_wglGetCurrentContext() == nullptr)
|
if (!g_gl_resolved || g_wglGetCurrentContext() == nullptr) {
|
||||||
{
|
if (!g_unsupported_logged) {
|
||||||
if (!g_unsupported_logged)
|
|
||||||
{
|
|
||||||
logf("opengl: no current GL context / glReadPixels; capture idle");
|
logf("opengl: no current GL context / glReadPixels; capture idle");
|
||||||
g_unsupported_logged = true;
|
g_unsupported_logged = true;
|
||||||
}
|
}
|
||||||
@@ -112,32 +104,27 @@ void capture_gl(HDC hdc)
|
|||||||
|
|
||||||
HWND hwnd = WindowFromDC(hdc);
|
HWND hwnd = WindowFromDC(hdc);
|
||||||
RECT rc{};
|
RECT rc{};
|
||||||
if (hwnd == nullptr || !GetClientRect(hwnd, &rc))
|
if (hwnd == nullptr || !GetClientRect(hwnd, &rc)) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const UINT w = static_cast<UINT>(rc.right - rc.left);
|
const UINT w = static_cast<UINT>(rc.right - rc.left);
|
||||||
const UINT h = static_cast<UINT>(rc.bottom - rc.top);
|
const UINT h = static_cast<UINT>(rc.bottom - rc.top);
|
||||||
if (w == 0 || h == 0)
|
if (w == 0 || h == 0) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DXGI_FORMAT_R8G8B8A8_UNORM matches glReadPixels(GL_RGBA) byte order.
|
// DXGI_FORMAT_R8G8B8A8_UNORM matches glReadPixels(GL_RGBA) byte order.
|
||||||
if (!ensure_device() || !g_shared.ensure(g_device, w, h, DXGI_FORMAT_R8G8B8A8_UNORM, g_pid, "opengl"))
|
if (!ensure_device() || !g_shared.ensure(g_device, w, h, DXGI_FORMAT_R8G8B8A8_UNORM, g_pid, "opengl")) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t bytes = static_cast<size_t>(w) * h * 4;
|
const size_t bytes = static_cast<size_t>(w) * h * 4;
|
||||||
if (g_read_buf.size() != bytes)
|
if (g_read_buf.size() != bytes) {
|
||||||
{
|
|
||||||
g_read_buf.resize(bytes);
|
g_read_buf.resize(bytes);
|
||||||
g_flip_buf.resize(bytes);
|
g_flip_buf.resize(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g_glPixelStorei != nullptr)
|
if (g_glPixelStorei != nullptr) {
|
||||||
{
|
|
||||||
g_glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
g_glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||||
}
|
}
|
||||||
// Reads the back buffer of the current context (bottom-up, origin lower-left).
|
// Reads the back buffer of the current context (bottom-up, origin lower-left).
|
||||||
@@ -145,19 +132,16 @@ void capture_gl(HDC hdc)
|
|||||||
|
|
||||||
// Flip vertically so the image is top-down like a D3D backbuffer.
|
// Flip vertically so the image is top-down like a D3D backbuffer.
|
||||||
const size_t row = static_cast<size_t>(w) * 4;
|
const size_t row = static_cast<size_t>(w) * 4;
|
||||||
for (UINT y = 0; y < h; ++y)
|
for (UINT y = 0; y < h; ++y) {
|
||||||
{
|
|
||||||
memcpy(g_flip_buf.data() + y * row, g_read_buf.data() + (h - 1 - y) * row, row);
|
memcpy(g_flip_buf.data() + y * row, g_read_buf.data() + (h - 1 - y) * row, row);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK)
|
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK) {
|
||||||
{
|
|
||||||
g_ctx->UpdateSubresource(g_shared.texture(), 0, nullptr, g_flip_buf.data(), static_cast<UINT>(row), 0);
|
g_ctx->UpdateSubresource(g_shared.texture(), 0, nullptr, g_flip_buf.data(), static_cast<UINT>(row), 0);
|
||||||
g_ctx->Flush();
|
g_ctx->Flush();
|
||||||
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
|
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
|
||||||
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
|
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->publish_video_frame(w, h, static_cast<std::uint32_t>(DXGI_FORMAT_R8G8B8A8_UNORM));
|
g_ipc->publish_video_frame(w, h, static_cast<std::uint32_t>(DXGI_FORMAT_R8G8B8A8_UNORM));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,18 +154,15 @@ BOOL swap_detour(safetyhook::InlineHook& hook, int hook_id, HDC hdc)
|
|||||||
hook_note_call(hook_id);
|
hook_note_call(hook_id);
|
||||||
g_swaps.fetch_add(1, std::memory_order_relaxed);
|
g_swaps.fetch_add(1, std::memory_order_relaxed);
|
||||||
const bool outer = !t_in_swap;
|
const bool outer = !t_in_swap;
|
||||||
if (outer)
|
if (outer) {
|
||||||
{
|
|
||||||
t_in_swap = true;
|
t_in_swap = true;
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->note_present();
|
g_ipc->note_present();
|
||||||
}
|
}
|
||||||
capture_gl(hdc);
|
capture_gl(hdc);
|
||||||
}
|
}
|
||||||
const BOOL r = hook.stdcall<BOOL>(hdc); // __stdcall: call() is __cdecl on x86 -> crash
|
const BOOL r = hook.stdcall<BOOL>(hdc); // __stdcall: call() is __cdecl on x86 -> crash
|
||||||
if (outer)
|
if (outer) {
|
||||||
{
|
|
||||||
t_in_swap = false;
|
t_in_swap = false;
|
||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
@@ -205,8 +186,7 @@ bool install_opengl_hooks(IpcClient& ipc)
|
|||||||
{
|
{
|
||||||
g_ipc = &ipc;
|
g_ipc = &ipc;
|
||||||
g_pid = GetCurrentProcessId();
|
g_pid = GetCurrentProcessId();
|
||||||
if (g_hk_swapbuffers.enabled() || g_hk_wglswap.enabled())
|
if (g_hk_swapbuffers.enabled() || g_hk_wglswap.enabled()) {
|
||||||
{
|
|
||||||
return true; // already installed (persistent hooks; re-install below re-enables them)
|
return true; // already installed (persistent hooks; re-install below re-enables them)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,18 +195,14 @@ bool install_opengl_hooks(IpcClient& ipc)
|
|||||||
g_unsupported_logged = false;
|
g_unsupported_logged = false;
|
||||||
|
|
||||||
// gdi32!SwapBuffers is always available (the common GL present call).
|
// gdi32!SwapBuffers is always available (the common GL present call).
|
||||||
if (HMODULE gdi = GetModuleHandleW(L"gdi32.dll"))
|
if (HMODULE gdi = GetModuleHandleW(L"gdi32.dll")) {
|
||||||
{
|
if (void* fn = reinterpret_cast<void*>(GetProcAddress(gdi, "SwapBuffers"))) {
|
||||||
if (void* fn = reinterpret_cast<void*>(GetProcAddress(gdi, "SwapBuffers")))
|
|
||||||
{
|
|
||||||
install_inline(g_hk_swapbuffers, fn, &hk_SwapBuffers);
|
install_inline(g_hk_swapbuffers, fn, &hk_SwapBuffers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// opengl32!wglSwapBuffers if OpenGL is already loaded.
|
// opengl32!wglSwapBuffers if OpenGL is already loaded.
|
||||||
if (HMODULE gl = GetModuleHandleW(L"opengl32.dll"))
|
if (HMODULE gl = GetModuleHandleW(L"opengl32.dll")) {
|
||||||
{
|
if (void* fn = reinterpret_cast<void*>(GetProcAddress(gl, "wglSwapBuffers"))) {
|
||||||
if (void* fn = reinterpret_cast<void*>(GetProcAddress(gl, "wglSwapBuffers")))
|
|
||||||
{
|
|
||||||
install_inline(g_hk_wglswap, fn, &hk_wglSwapBuffers);
|
install_inline(g_hk_wglswap, fn, &hk_wglSwapBuffers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,13 +227,11 @@ void remove_opengl_hooks()
|
|||||||
hook_set_installed(g_id_wglswap, false);
|
hook_set_installed(g_id_wglswap, false);
|
||||||
g_gate.drain();
|
g_gate.drain();
|
||||||
g_shared.release();
|
g_shared.release();
|
||||||
if (g_ctx != nullptr)
|
if (g_ctx != nullptr) {
|
||||||
{
|
|
||||||
g_ctx->Release();
|
g_ctx->Release();
|
||||||
g_ctx = nullptr;
|
g_ctx = nullptr;
|
||||||
}
|
}
|
||||||
if (g_device != nullptr)
|
if (g_device != nullptr) {
|
||||||
{
|
|
||||||
g_device->Release();
|
g_device->Release();
|
||||||
g_device = nullptr;
|
g_device = nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
|
|
||||||
#include "ipc_client.hpp"
|
#include "ipc_client.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
// Installs the OpenGL swap hooks. `ipc` must outlive the hooks. Returns true if at
|
// Installs the OpenGL swap hooks. `ipc` must outlive the hooks. Returns true if at
|
||||||
// least SwapBuffers was hooked. Safe to call repeatedly.
|
// least SwapBuffers was hooked. Safe to call repeatedly.
|
||||||
|
|||||||
@@ -22,11 +22,9 @@
|
|||||||
#include "shared_video_texture.hpp"
|
#include "shared_video_texture.hpp"
|
||||||
#include "vtable_hook.hpp"
|
#include "vtable_hook.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
DetourGate g_gate; // drains in-flight Present/ECL detours before remove frees the shared state
|
DetourGate g_gate; // drains in-flight Present/ECL detours before remove frees the shared state
|
||||||
|
|
||||||
@@ -116,8 +114,7 @@ std::atomic<ID3D12CommandQueue*> g_present_queue{nullptr};
|
|||||||
// test present is counted but produces no frame. Render-thread only; small fixed tables.
|
// test present is counted but produces no frame. Render-thread only; small fixed tables.
|
||||||
constexpr int kMaxLoggedPresents = 16;
|
constexpr int kMaxLoggedPresents = 16;
|
||||||
constexpr int kMaxLoggedSwapchains = 8;
|
constexpr int kMaxLoggedSwapchains = 8;
|
||||||
struct LoggedPresent
|
struct LoggedPresent {
|
||||||
{
|
|
||||||
void* swapchain;
|
void* swapchain;
|
||||||
UINT flags;
|
UINT flags;
|
||||||
};
|
};
|
||||||
@@ -129,15 +126,12 @@ int g_logged_swapchains_n = 0;
|
|||||||
// True the first time this (swapchain, flags) pair is presented, so the caller logs once.
|
// True the first time this (swapchain, flags) pair is presented, so the caller logs once.
|
||||||
bool first_present_with_flags(void* swapchain, UINT flags)
|
bool first_present_with_flags(void* swapchain, UINT flags)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < g_logged_presents_n; ++i)
|
for (int i = 0; i < g_logged_presents_n; ++i) {
|
||||||
{
|
if (g_logged_presents[i].swapchain == swapchain && g_logged_presents[i].flags == flags) {
|
||||||
if (g_logged_presents[i].swapchain == swapchain && g_logged_presents[i].flags == flags)
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (g_logged_presents_n >= kMaxLoggedPresents)
|
if (g_logged_presents_n >= kMaxLoggedPresents) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
g_logged_presents[g_logged_presents_n++] = {swapchain, flags};
|
g_logged_presents[g_logged_presents_n++] = {swapchain, flags};
|
||||||
@@ -147,15 +141,12 @@ bool first_present_with_flags(void* swapchain, UINT flags)
|
|||||||
// True the first time this swapchain feeds the capture, so the caller logs it once.
|
// True the first time this swapchain feeds the capture, so the caller logs it once.
|
||||||
bool first_capture_from(void* swapchain)
|
bool first_capture_from(void* swapchain)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < g_logged_swapchains_n; ++i)
|
for (int i = 0; i < g_logged_swapchains_n; ++i) {
|
||||||
{
|
if (g_logged_swapchains[i] == swapchain) {
|
||||||
if (g_logged_swapchains[i] == swapchain)
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (g_logged_swapchains_n >= kMaxLoggedSwapchains)
|
if (g_logged_swapchains_n >= kMaxLoggedSwapchains) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
g_logged_swapchains[g_logged_swapchains_n++] = swapchain;
|
g_logged_swapchains[g_logged_swapchains_n++] = swapchain;
|
||||||
@@ -165,33 +156,27 @@ bool first_capture_from(void* swapchain)
|
|||||||
// Drop the D3D11On12 bridge. Caller holds g_tex_mutex.
|
// Drop the D3D11On12 bridge. Caller holds g_tex_mutex.
|
||||||
void release_on12_locked()
|
void release_on12_locked()
|
||||||
{
|
{
|
||||||
if (g_on12_ctx != nullptr)
|
if (g_on12_ctx != nullptr) {
|
||||||
{
|
|
||||||
g_on12_ctx->Release();
|
g_on12_ctx->Release();
|
||||||
g_on12_ctx = nullptr;
|
g_on12_ctx = nullptr;
|
||||||
}
|
}
|
||||||
if (g_on12 != nullptr)
|
if (g_on12 != nullptr) {
|
||||||
{
|
|
||||||
g_on12->Release();
|
g_on12->Release();
|
||||||
g_on12 = nullptr;
|
g_on12 = nullptr;
|
||||||
}
|
}
|
||||||
if (g_on12_d3d11 != nullptr)
|
if (g_on12_d3d11 != nullptr) {
|
||||||
{
|
|
||||||
g_on12_d3d11->Release();
|
g_on12_d3d11->Release();
|
||||||
g_on12_d3d11 = nullptr;
|
g_on12_d3d11 = nullptr;
|
||||||
}
|
}
|
||||||
if (g_on12_queue != nullptr)
|
if (g_on12_queue != nullptr) {
|
||||||
{
|
|
||||||
g_on12_queue->Release();
|
g_on12_queue->Release();
|
||||||
g_on12_queue = nullptr;
|
g_on12_queue = nullptr;
|
||||||
}
|
}
|
||||||
if (g_on12_d3d12 != nullptr)
|
if (g_on12_d3d12 != nullptr) {
|
||||||
{
|
|
||||||
g_on12_d3d12->Release();
|
g_on12_d3d12->Release();
|
||||||
g_on12_d3d12 = nullptr;
|
g_on12_d3d12 = nullptr;
|
||||||
}
|
}
|
||||||
if (g_copy_fence != nullptr)
|
if (g_copy_fence != nullptr) {
|
||||||
{
|
|
||||||
g_copy_fence->Release();
|
g_copy_fence->Release();
|
||||||
g_copy_fence = nullptr;
|
g_copy_fence = nullptr;
|
||||||
}
|
}
|
||||||
@@ -205,8 +190,7 @@ void release_on12_locked()
|
|||||||
// holds g_tex_mutex. Returns true when the bridge is ready.
|
// holds g_tex_mutex. Returns true when the bridge is ready.
|
||||||
bool ensure_on12_locked(ID3D12Device* dev)
|
bool ensure_on12_locked(ID3D12Device* dev)
|
||||||
{
|
{
|
||||||
if (g_on12 != nullptr && g_on12_d3d12 == dev)
|
if (g_on12 != nullptr && g_on12_d3d12 == dev) {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
release_on12_locked();
|
release_on12_locked();
|
||||||
@@ -215,8 +199,7 @@ bool ensure_on12_locked(ID3D12Device* dev)
|
|||||||
qd.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
|
qd.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
|
||||||
ID3D12CommandQueue* queue = nullptr;
|
ID3D12CommandQueue* queue = nullptr;
|
||||||
HRESULT hr = dev->CreateCommandQueue(&qd, __uuidof(ID3D12CommandQueue), reinterpret_cast<void**>(&queue));
|
HRESULT hr = dev->CreateCommandQueue(&qd, __uuidof(ID3D12CommandQueue), reinterpret_cast<void**>(&queue));
|
||||||
if (FAILED(hr) || queue == nullptr)
|
if (FAILED(hr) || queue == nullptr) {
|
||||||
{
|
|
||||||
logf("present(d3d12): CreateCommandQueue failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
logf("present(d3d12): CreateCommandQueue failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -225,19 +208,16 @@ bool ensure_on12_locked(ID3D12Device* dev)
|
|||||||
ID3D11Device* d11 = nullptr;
|
ID3D11Device* d11 = nullptr;
|
||||||
ID3D11DeviceContext* ctx = nullptr;
|
ID3D11DeviceContext* ctx = nullptr;
|
||||||
hr = D3D11On12CreateDevice(dev, 0, nullptr, 0, queues, 1, 0, &d11, &ctx, nullptr);
|
hr = D3D11On12CreateDevice(dev, 0, nullptr, 0, queues, 1, 0, &d11, &ctx, nullptr);
|
||||||
if (FAILED(hr) || d11 == nullptr)
|
if (FAILED(hr) || d11 == nullptr) {
|
||||||
{
|
|
||||||
logf("present(d3d12): D3D11On12CreateDevice failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
logf("present(d3d12): D3D11On12CreateDevice failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
||||||
queue->Release();
|
queue->Release();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ID3D11On12Device* on12 = nullptr;
|
ID3D11On12Device* on12 = nullptr;
|
||||||
hr = d11->QueryInterface(__uuidof(ID3D11On12Device), reinterpret_cast<void**>(&on12));
|
hr = d11->QueryInterface(__uuidof(ID3D11On12Device), reinterpret_cast<void**>(&on12));
|
||||||
if (FAILED(hr) || on12 == nullptr)
|
if (FAILED(hr) || on12 == nullptr) {
|
||||||
{
|
|
||||||
logf("present(d3d12): QI ID3D11On12Device failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
logf("present(d3d12): QI ID3D11On12Device failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
||||||
if (ctx != nullptr)
|
if (ctx != nullptr) {
|
||||||
{
|
|
||||||
ctx->Release();
|
ctx->Release();
|
||||||
}
|
}
|
||||||
d11->Release();
|
d11->Release();
|
||||||
@@ -249,10 +229,8 @@ bool ensure_on12_locked(ID3D12Device* dev)
|
|||||||
// cross-queue ordering (the copy can then race the frame, the pre-fence behavior).
|
// cross-queue ordering (the copy can then race the frame, the pre-fence behavior).
|
||||||
ID3D12Fence* fence = nullptr;
|
ID3D12Fence* fence = nullptr;
|
||||||
hr = dev->CreateFence(0, D3D12_FENCE_FLAG_NONE, __uuidof(ID3D12Fence), reinterpret_cast<void**>(&fence));
|
hr = dev->CreateFence(0, D3D12_FENCE_FLAG_NONE, __uuidof(ID3D12Fence), reinterpret_cast<void**>(&fence));
|
||||||
if (FAILED(hr) || fence == nullptr)
|
if (FAILED(hr) || fence == nullptr) {
|
||||||
{
|
logf("present(d3d12): CreateFence failed hr=0x%08lX (copy will be unordered)", static_cast<unsigned long>(hr));
|
||||||
logf("present(d3d12): CreateFence failed hr=0x%08lX (copy will be unordered)",
|
|
||||||
static_cast<unsigned long>(hr));
|
|
||||||
fence = nullptr;
|
fence = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,17 +261,14 @@ void capture_backbuffer_d3d12(IDXGISwapChain* sc)
|
|||||||
// buffer. Query IDXGISwapChain3 for it; fall back to 0 only if unavailable.
|
// buffer. Query IDXGISwapChain3 for it; fall back to 0 only if unavailable.
|
||||||
UINT bb_index = 0;
|
UINT bb_index = 0;
|
||||||
IDXGISwapChain3* sc3 = nullptr;
|
IDXGISwapChain3* sc3 = nullptr;
|
||||||
if (SUCCEEDED(sc->QueryInterface(__uuidof(IDXGISwapChain3), reinterpret_cast<void**>(&sc3))) && sc3 != nullptr)
|
if (SUCCEEDED(sc->QueryInterface(__uuidof(IDXGISwapChain3), reinterpret_cast<void**>(&sc3))) && sc3 != nullptr) {
|
||||||
{
|
|
||||||
bb_index = sc3->GetCurrentBackBufferIndex();
|
bb_index = sc3->GetCurrentBackBufferIndex();
|
||||||
sc3->Release();
|
sc3->Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
ID3D12Resource* bb = nullptr;
|
ID3D12Resource* bb = nullptr;
|
||||||
if (FAILED(sc->GetBuffer(bb_index, __uuidof(ID3D12Resource), reinterpret_cast<void**>(&bb))) || bb == nullptr)
|
if (FAILED(sc->GetBuffer(bb_index, __uuidof(ID3D12Resource), reinterpret_cast<void**>(&bb))) || bb == nullptr) {
|
||||||
{
|
if (!g_unsupported_logged) {
|
||||||
if (!g_unsupported_logged)
|
|
||||||
{
|
|
||||||
logf("present: backbuffer is neither ID3D11Texture2D nor ID3D12Resource (D3D9/Vulkan?); idle");
|
logf("present: backbuffer is neither ID3D11Texture2D nor ID3D12Resource (D3D9/Vulkan?); idle");
|
||||||
g_unsupported_logged = true;
|
g_unsupported_logged = true;
|
||||||
}
|
}
|
||||||
@@ -309,25 +284,21 @@ void capture_backbuffer_d3d12(IDXGISwapChain* sc)
|
|||||||
DXGI_FORMAT fmt = DXGI_FORMAT_UNKNOWN;
|
DXGI_FORMAT fmt = DXGI_FORMAT_UNKNOWN;
|
||||||
// The game's present queue, preferring the one seen on this (the render) thread.
|
// The game's present queue, preferring the one seen on this (the render) thread.
|
||||||
ID3D12CommandQueue* game_queue = t_present_queue;
|
ID3D12CommandQueue* game_queue = t_present_queue;
|
||||||
if (game_queue == nullptr)
|
if (game_queue == nullptr) {
|
||||||
{
|
|
||||||
game_queue = g_present_queue.load(std::memory_order_relaxed);
|
game_queue = g_present_queue.load(std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log each distinct swapchain feeding the capture once (size/format/buffer index).
|
// Log each distinct swapchain feeding the capture once (size/format/buffer index).
|
||||||
if (first_capture_from(sc))
|
if (first_capture_from(sc)) {
|
||||||
{
|
|
||||||
const D3D12_RESOURCE_DESC rd = bb->GetDesc();
|
const D3D12_RESOURCE_DESC rd = bb->GetDesc();
|
||||||
logf("present: swapchain=%p capturing D3D12 backbuffer %llux%u fmt=%d samples=%u bufferindex=%u queue=%s",
|
logf("present: swapchain=%p capturing D3D12 backbuffer %llux%u fmt=%d samples=%u bufferindex=%u queue=%s", sc,
|
||||||
sc, static_cast<unsigned long long>(rd.Width), rd.Height, static_cast<int>(rd.Format),
|
static_cast<unsigned long long>(rd.Width), rd.Height, static_cast<int>(rd.Format), rd.SampleDesc.Count,
|
||||||
rd.SampleDesc.Count, bb_index, game_queue != nullptr ? "known" : "unknown");
|
bb_index, game_queue != nullptr ? "known" : "unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dev != nullptr)
|
if (dev != nullptr) {
|
||||||
{
|
|
||||||
std::scoped_lock lock(g_tex_mutex);
|
std::scoped_lock lock(g_tex_mutex);
|
||||||
if (ensure_on12_locked(dev))
|
if (ensure_on12_locked(dev)) {
|
||||||
{
|
|
||||||
// DX12 capture costs more present-thread overhead than DX11/OpenGL (~0.38 ms vs
|
// DX12 capture costs more present-thread overhead than DX11/OpenGL (~0.38 ms vs
|
||||||
// ~0.05/0.09 ms, measured) because it goes through the D3D11On12 bridge: the
|
// ~0.05/0.09 ms, measured) because it goes through the D3D11On12 bridge: the
|
||||||
// CopyResource on the 11On12 immediate context (~0.13 ms) plus the mandatory Flush
|
// CopyResource on the 11On12 immediate context (~0.13 ms) plus the mandatory Flush
|
||||||
@@ -339,8 +310,7 @@ void capture_backbuffer_d3d12(IDXGISwapChain* sc)
|
|||||||
// Order our copy after the game's frame without burdening the game's queue: the
|
// Order our copy after the game's frame without burdening the game's queue: the
|
||||||
// game queue signals the fence (cheap), our copy queue waits on it. Skipped if the
|
// game queue signals the fence (cheap), our copy queue waits on it. Skipped if the
|
||||||
// queue isn't captured yet or the fence is missing (one possibly-early frame).
|
// queue isn't captured yet or the fence is missing (one possibly-early frame).
|
||||||
if (game_queue != nullptr && g_copy_fence != nullptr)
|
if (game_queue != nullptr && g_copy_fence != nullptr) {
|
||||||
{
|
|
||||||
const UINT64 fence_val = ++g_copy_fence_val;
|
const UINT64 fence_val = ++g_copy_fence_val;
|
||||||
game_queue->Signal(g_copy_fence, fence_val);
|
game_queue->Signal(g_copy_fence, fence_val);
|
||||||
g_on12_queue->Wait(g_copy_fence, fence_val);
|
g_on12_queue->Wait(g_copy_fence, fence_val);
|
||||||
@@ -349,33 +319,26 @@ void capture_backbuffer_d3d12(IDXGISwapChain* sc)
|
|||||||
D3D11_RESOURCE_FLAGS rf{};
|
D3D11_RESOURCE_FLAGS rf{};
|
||||||
rf.BindFlags = D3D11_BIND_RENDER_TARGET;
|
rf.BindFlags = D3D11_BIND_RENDER_TARGET;
|
||||||
ID3D11Resource* wrapped = nullptr;
|
ID3D11Resource* wrapped = nullptr;
|
||||||
HRESULT hr = g_on12->CreateWrappedResource(bb, &rf, D3D12_RESOURCE_STATE_PRESENT,
|
HRESULT hr =
|
||||||
D3D12_RESOURCE_STATE_PRESENT, __uuidof(ID3D11Resource),
|
g_on12->CreateWrappedResource(bb, &rf, D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_PRESENT,
|
||||||
reinterpret_cast<void**>(&wrapped));
|
__uuidof(ID3D11Resource), reinterpret_cast<void**>(&wrapped));
|
||||||
if (SUCCEEDED(hr) && wrapped != nullptr)
|
if (SUCCEEDED(hr) && wrapped != nullptr) {
|
||||||
{
|
|
||||||
g_on12->AcquireWrappedResources(&wrapped, 1);
|
g_on12->AcquireWrappedResources(&wrapped, 1);
|
||||||
ID3D11Texture2D* wtex = nullptr;
|
ID3D11Texture2D* wtex = nullptr;
|
||||||
if (SUCCEEDED(wrapped->QueryInterface(__uuidof(ID3D11Texture2D),
|
if (SUCCEEDED(wrapped->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&wtex)))
|
||||||
reinterpret_cast<void**>(&wtex))) &&
|
&& wtex != nullptr) {
|
||||||
wtex != nullptr)
|
|
||||||
{
|
|
||||||
D3D11_TEXTURE2D_DESC d{};
|
D3D11_TEXTURE2D_DESC d{};
|
||||||
wtex->GetDesc(&d);
|
wtex->GetDesc(&d);
|
||||||
w = d.Width;
|
w = d.Width;
|
||||||
h = d.Height;
|
h = d.Height;
|
||||||
fmt = d.Format;
|
fmt = d.Format;
|
||||||
if (d.SampleDesc.Count == 1 &&
|
if (d.SampleDesc.Count == 1
|
||||||
g_shared.ensure(g_on12_d3d11, w, h, fmt, g_pid, "present", kShareBind))
|
&& g_shared.ensure(g_on12_d3d11, w, h, fmt, g_pid, "present", kShareBind)) {
|
||||||
{
|
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 0) == S_OK) {
|
||||||
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 0) == S_OK)
|
|
||||||
{
|
|
||||||
g_on12_ctx->CopyResource(g_shared.texture(), wtex);
|
g_on12_ctx->CopyResource(g_shared.texture(), wtex);
|
||||||
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
|
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
|
||||||
shared = true;
|
shared = true;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
dropped = true; // host held the mutex -> this frame never reaches the mirror
|
dropped = true; // host held the mutex -> this frame never reaches the mirror
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,29 +347,22 @@ void capture_backbuffer_d3d12(IDXGISwapChain* sc)
|
|||||||
g_on12->ReleaseWrappedResources(&wrapped, 1);
|
g_on12->ReleaseWrappedResources(&wrapped, 1);
|
||||||
g_on12_ctx->Flush();
|
g_on12_ctx->Flush();
|
||||||
wrapped->Release();
|
wrapped->Release();
|
||||||
}
|
} else if (!g_unsupported_logged) {
|
||||||
else if (!g_unsupported_logged)
|
|
||||||
{
|
|
||||||
logf("present(d3d12): CreateWrappedResource failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
logf("present(d3d12): CreateWrappedResource failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
||||||
g_unsupported_logged = true;
|
g_unsupported_logged = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shared)
|
if (shared) {
|
||||||
{
|
|
||||||
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
|
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->publish_video_frame(w, h, static_cast<std::uint32_t>(fmt));
|
g_ipc->publish_video_frame(w, h, static_cast<std::uint32_t>(fmt));
|
||||||
}
|
}
|
||||||
}
|
} else if (dropped && g_ipc != nullptr) {
|
||||||
else if (dropped && g_ipc != nullptr)
|
|
||||||
{
|
|
||||||
g_ipc->note_video_dropped();
|
g_ipc->note_video_dropped();
|
||||||
}
|
}
|
||||||
if (dev != nullptr)
|
if (dev != nullptr) {
|
||||||
{
|
|
||||||
dev->Release();
|
dev->Release();
|
||||||
}
|
}
|
||||||
bb->Release();
|
bb->Release();
|
||||||
@@ -415,26 +371,22 @@ void capture_backbuffer_d3d12(IDXGISwapChain* sc)
|
|||||||
// Drop the hook-owned D3D11 device and the D3D10 staging texture. Caller holds g_tex_mutex.
|
// Drop the hook-owned D3D11 device and the D3D10 staging texture. Caller holds g_tex_mutex.
|
||||||
void release_aux_locked()
|
void release_aux_locked()
|
||||||
{
|
{
|
||||||
if (g_d3d10_staging != nullptr)
|
if (g_d3d10_staging != nullptr) {
|
||||||
{
|
|
||||||
g_d3d10_staging->Release();
|
g_d3d10_staging->Release();
|
||||||
g_d3d10_staging = nullptr;
|
g_d3d10_staging = nullptr;
|
||||||
}
|
}
|
||||||
if (g_d3d10_dev != nullptr)
|
if (g_d3d10_dev != nullptr) {
|
||||||
{
|
|
||||||
g_d3d10_dev->Release();
|
g_d3d10_dev->Release();
|
||||||
g_d3d10_dev = nullptr;
|
g_d3d10_dev = nullptr;
|
||||||
}
|
}
|
||||||
g_d3d10_w = g_d3d10_h = 0;
|
g_d3d10_w = g_d3d10_h = 0;
|
||||||
g_d3d10_fmt = DXGI_FORMAT_UNKNOWN;
|
g_d3d10_fmt = DXGI_FORMAT_UNKNOWN;
|
||||||
g_force_d3d10 = false;
|
g_force_d3d10 = false;
|
||||||
if (g_aux_ctx != nullptr)
|
if (g_aux_ctx != nullptr) {
|
||||||
{
|
|
||||||
g_aux_ctx->Release();
|
g_aux_ctx->Release();
|
||||||
g_aux_ctx = nullptr;
|
g_aux_ctx = nullptr;
|
||||||
}
|
}
|
||||||
if (g_aux_d3d11 != nullptr)
|
if (g_aux_d3d11 != nullptr) {
|
||||||
{
|
|
||||||
g_aux_d3d11->Release();
|
g_aux_d3d11->Release();
|
||||||
g_aux_d3d11 = nullptr;
|
g_aux_d3d11 = nullptr;
|
||||||
}
|
}
|
||||||
@@ -444,16 +396,14 @@ void release_aux_locked()
|
|||||||
// (the game has no D3D11 device of its own). Caller holds g_tex_mutex.
|
// (the game has no D3D11 device of its own). Caller holds g_tex_mutex.
|
||||||
bool ensure_aux_d3d11_locked()
|
bool ensure_aux_d3d11_locked()
|
||||||
{
|
{
|
||||||
if (g_aux_d3d11 != nullptr)
|
if (g_aux_d3d11 != nullptr) {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const D3D_FEATURE_LEVEL levels[] = {D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0};
|
const D3D_FEATURE_LEVEL levels[] = {D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0};
|
||||||
HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, levels,
|
HRESULT hr =
|
||||||
static_cast<UINT>(std::size(levels)), D3D11_SDK_VERSION, &g_aux_d3d11, nullptr,
|
D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, levels, static_cast<UINT>(std::size(levels)),
|
||||||
&g_aux_ctx);
|
D3D11_SDK_VERSION, &g_aux_d3d11, nullptr, &g_aux_ctx);
|
||||||
if (FAILED(hr) || g_aux_d3d11 == nullptr)
|
if (FAILED(hr) || g_aux_d3d11 == nullptr) {
|
||||||
{
|
|
||||||
logf("present(d3d10): aux D3D11CreateDevice failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
logf("present(d3d10): aux D3D11CreateDevice failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
||||||
g_aux_d3d11 = nullptr;
|
g_aux_d3d11 = nullptr;
|
||||||
g_aux_ctx = nullptr;
|
g_aux_ctx = nullptr;
|
||||||
@@ -470,20 +420,17 @@ void capture_backbuffer_d3d10(IDXGISwapChain* sc, ID3D10Texture2D* backbuf)
|
|||||||
{
|
{
|
||||||
D3D10_TEXTURE2D_DESC bd{};
|
D3D10_TEXTURE2D_DESC bd{};
|
||||||
backbuf->GetDesc(&bd);
|
backbuf->GetDesc(&bd);
|
||||||
if (first_capture_from(sc))
|
if (first_capture_from(sc)) {
|
||||||
{
|
|
||||||
logf("present: swapchain=%p capturing D3D10 backbuffer %ux%u fmt=%d samples=%u", sc, bd.Width, bd.Height,
|
logf("present: swapchain=%p capturing D3D10 backbuffer %ux%u fmt=%d samples=%u", sc, bd.Width, bd.Height,
|
||||||
static_cast<int>(bd.Format), bd.SampleDesc.Count);
|
static_cast<int>(bd.Format), bd.SampleDesc.Count);
|
||||||
}
|
}
|
||||||
if (bd.SampleDesc.Count != 1)
|
if (bd.SampleDesc.Count != 1) {
|
||||||
{
|
|
||||||
return; // MSAA: would need ResolveSubresource; skip rather than mis-copy
|
return; // MSAA: would need ResolveSubresource; skip rather than mis-copy
|
||||||
}
|
}
|
||||||
|
|
||||||
ID3D10Device* gdev = nullptr;
|
ID3D10Device* gdev = nullptr;
|
||||||
backbuf->GetDevice(&gdev);
|
backbuf->GetDevice(&gdev);
|
||||||
if (gdev == nullptr)
|
if (gdev == nullptr) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,16 +438,13 @@ void capture_backbuffer_d3d10(IDXGISwapChain* sc, ID3D10Texture2D* backbuf)
|
|||||||
bool dropped = false;
|
bool dropped = false;
|
||||||
{
|
{
|
||||||
std::scoped_lock lock(g_tex_mutex);
|
std::scoped_lock lock(g_tex_mutex);
|
||||||
if (!(g_d3d10_staging != nullptr && g_d3d10_dev == gdev && g_d3d10_w == bd.Width &&
|
if (!(g_d3d10_staging != nullptr && g_d3d10_dev == gdev && g_d3d10_w == bd.Width && g_d3d10_h == bd.Height
|
||||||
g_d3d10_h == bd.Height && g_d3d10_fmt == bd.Format))
|
&& g_d3d10_fmt == bd.Format)) {
|
||||||
{
|
if (g_d3d10_staging != nullptr) {
|
||||||
if (g_d3d10_staging != nullptr)
|
|
||||||
{
|
|
||||||
g_d3d10_staging->Release();
|
g_d3d10_staging->Release();
|
||||||
g_d3d10_staging = nullptr;
|
g_d3d10_staging = nullptr;
|
||||||
}
|
}
|
||||||
if (g_d3d10_dev != nullptr)
|
if (g_d3d10_dev != nullptr) {
|
||||||
{
|
|
||||||
g_d3d10_dev->Release();
|
g_d3d10_dev->Release();
|
||||||
g_d3d10_dev = nullptr;
|
g_d3d10_dev = nullptr;
|
||||||
}
|
}
|
||||||
@@ -513,8 +457,7 @@ void capture_backbuffer_d3d10(IDXGISwapChain* sc, ID3D10Texture2D* backbuf)
|
|||||||
sd.SampleDesc.Count = 1;
|
sd.SampleDesc.Count = 1;
|
||||||
sd.Usage = D3D10_USAGE_STAGING;
|
sd.Usage = D3D10_USAGE_STAGING;
|
||||||
sd.CPUAccessFlags = D3D10_CPU_ACCESS_READ;
|
sd.CPUAccessFlags = D3D10_CPU_ACCESS_READ;
|
||||||
if (SUCCEEDED(gdev->CreateTexture2D(&sd, nullptr, &g_d3d10_staging)) && g_d3d10_staging != nullptr)
|
if (SUCCEEDED(gdev->CreateTexture2D(&sd, nullptr, &g_d3d10_staging)) && g_d3d10_staging != nullptr) {
|
||||||
{
|
|
||||||
g_d3d10_dev = gdev;
|
g_d3d10_dev = gdev;
|
||||||
gdev->AddRef();
|
gdev->AddRef();
|
||||||
g_d3d10_w = bd.Width;
|
g_d3d10_w = bd.Width;
|
||||||
@@ -523,21 +466,16 @@ void capture_backbuffer_d3d10(IDXGISwapChain* sc, ID3D10Texture2D* backbuf)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g_d3d10_staging != nullptr && ensure_aux_d3d11_locked() &&
|
if (g_d3d10_staging != nullptr && ensure_aux_d3d11_locked()
|
||||||
g_shared.ensure(g_aux_d3d11, bd.Width, bd.Height, bd.Format, g_pid, "present", kShareBind))
|
&& g_shared.ensure(g_aux_d3d11, bd.Width, bd.Height, bd.Format, g_pid, "present", kShareBind)) {
|
||||||
{
|
|
||||||
gdev->CopyResource(g_d3d10_staging, backbuf);
|
gdev->CopyResource(g_d3d10_staging, backbuf);
|
||||||
D3D10_MAPPED_TEXTURE2D m{};
|
D3D10_MAPPED_TEXTURE2D m{};
|
||||||
if (SUCCEEDED(g_d3d10_staging->Map(0, D3D10_MAP_READ, 0, &m)) && m.pData != nullptr)
|
if (SUCCEEDED(g_d3d10_staging->Map(0, D3D10_MAP_READ, 0, &m)) && m.pData != nullptr) {
|
||||||
{
|
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK) {
|
||||||
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK)
|
|
||||||
{
|
|
||||||
g_aux_ctx->UpdateSubresource(g_shared.texture(), 0, nullptr, m.pData, m.RowPitch, 0);
|
g_aux_ctx->UpdateSubresource(g_shared.texture(), 0, nullptr, m.pData, m.RowPitch, 0);
|
||||||
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
|
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
|
||||||
shared = true;
|
shared = true;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
dropped = true;
|
dropped = true;
|
||||||
}
|
}
|
||||||
g_d3d10_staging->Unmap(0);
|
g_d3d10_staging->Unmap(0);
|
||||||
@@ -545,16 +483,12 @@ void capture_backbuffer_d3d10(IDXGISwapChain* sc, ID3D10Texture2D* backbuf)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shared)
|
if (shared) {
|
||||||
{
|
|
||||||
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
|
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->publish_video_frame(bd.Width, bd.Height, static_cast<std::uint32_t>(bd.Format));
|
g_ipc->publish_video_frame(bd.Width, bd.Height, static_cast<std::uint32_t>(bd.Format));
|
||||||
}
|
}
|
||||||
}
|
} else if (dropped && g_ipc != nullptr) {
|
||||||
else if (dropped && g_ipc != nullptr)
|
|
||||||
{
|
|
||||||
g_ipc->note_video_dropped();
|
g_ipc->note_video_dropped();
|
||||||
}
|
}
|
||||||
gdev->Release();
|
gdev->Release();
|
||||||
@@ -568,12 +502,10 @@ void capture_backbuffer(IDXGISwapChain* sc)
|
|||||||
// ID3D11Texture2D (so we can't discriminate by GetBuffer), but its feature-level-10 device
|
// ID3D11Texture2D (so we can't discriminate by GetBuffer), but its feature-level-10 device
|
||||||
// rejects the share flags -- so a failed shared-texture creation is the signal to switch
|
// rejects the share flags -- so a failed shared-texture creation is the signal to switch
|
||||||
// (sticky) to the D3D10 read-back path, which reads through the game's own D3D10 device.
|
// (sticky) to the D3D10 read-back path, which reads through the game's own D3D10 device.
|
||||||
if (!g_force_d3d10)
|
if (!g_force_d3d10) {
|
||||||
{
|
|
||||||
ID3D11Texture2D* backbuf = nullptr;
|
ID3D11Texture2D* backbuf = nullptr;
|
||||||
if (FAILED(sc->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&backbuf))) ||
|
if (FAILED(sc->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&backbuf)))
|
||||||
backbuf == nullptr)
|
|| backbuf == nullptr) {
|
||||||
{
|
|
||||||
capture_backbuffer_d3d12(sc); // D3D12 game: bridge via D3D11On12 (or idle if neither)
|
capture_backbuffer_d3d12(sc); // D3D12 game: bridge via D3D11On12 (or idle if neither)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -583,8 +515,7 @@ void capture_backbuffer(IDXGISwapChain* sc)
|
|||||||
bool shared = false;
|
bool shared = false;
|
||||||
bool dropped = false;
|
bool dropped = false;
|
||||||
bool cant_host = false;
|
bool cant_host = false;
|
||||||
if (bd.SampleDesc.Count != 1)
|
if (bd.SampleDesc.Count != 1) {
|
||||||
{
|
|
||||||
backbuf->Release(); // MSAA would need ResolveSubresource; skip rather than mis-copy
|
backbuf->Release(); // MSAA would need ResolveSubresource; skip rather than mis-copy
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -592,62 +523,47 @@ void capture_backbuffer(IDXGISwapChain* sc)
|
|||||||
ID3D11Device* device = nullptr;
|
ID3D11Device* device = nullptr;
|
||||||
backbuf->GetDevice(&device);
|
backbuf->GetDevice(&device);
|
||||||
ID3D11DeviceContext* ctx = nullptr;
|
ID3D11DeviceContext* ctx = nullptr;
|
||||||
if (device != nullptr)
|
if (device != nullptr) {
|
||||||
{
|
|
||||||
device->GetImmediateContext(&ctx);
|
device->GetImmediateContext(&ctx);
|
||||||
}
|
}
|
||||||
if (device != nullptr && ctx != nullptr)
|
if (device != nullptr && ctx != nullptr) {
|
||||||
{
|
|
||||||
std::scoped_lock lock(g_tex_mutex);
|
std::scoped_lock lock(g_tex_mutex);
|
||||||
if (g_shared.ensure(device, bd.Width, bd.Height, bd.Format, g_pid, "present", kShareBind))
|
if (g_shared.ensure(device, bd.Width, bd.Height, bd.Format, g_pid, "present", kShareBind)) {
|
||||||
{
|
if (first_capture_from(sc)) {
|
||||||
if (first_capture_from(sc))
|
|
||||||
{
|
|
||||||
logf("present: swapchain=%p capturing D3D11 backbuffer %ux%u fmt=%d samples=%u", sc, bd.Width,
|
logf("present: swapchain=%p capturing D3D11 backbuffer %ux%u fmt=%d samples=%u", sc, bd.Width,
|
||||||
bd.Height, static_cast<int>(bd.Format), bd.SampleDesc.Count);
|
bd.Height, static_cast<int>(bd.Format), bd.SampleDesc.Count);
|
||||||
}
|
}
|
||||||
// Key 0 on both sides: a plain cross-process mutex on the texture (created
|
// Key 0 on both sides: a plain cross-process mutex on the texture (created
|
||||||
// released at key 0). Bounded wait so a stalled host consumer can never hang
|
// released at key 0). Bounded wait so a stalled host consumer can never hang
|
||||||
// the game's render thread.
|
// the game's render thread.
|
||||||
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK)
|
if (g_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK) {
|
||||||
{
|
|
||||||
ctx->CopyResource(g_shared.texture(), backbuf);
|
ctx->CopyResource(g_shared.texture(), backbuf);
|
||||||
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
|
g_shared.mutex()->ReleaseSync(kVideoMutexKey);
|
||||||
shared = true;
|
shared = true;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
dropped = true; // host held the mutex past the wait -> frame lost (rare on D3D11)
|
dropped = true; // host held the mutex past the wait -> frame lost (rare on D3D11)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
cant_host = true; // device can't host the shared texture -> try the D3D10 path
|
cant_host = true; // device can't host the shared texture -> try the D3D10 path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ctx != nullptr)
|
if (ctx != nullptr) {
|
||||||
{
|
|
||||||
ctx->Release();
|
ctx->Release();
|
||||||
}
|
}
|
||||||
if (device != nullptr)
|
if (device != nullptr) {
|
||||||
{
|
|
||||||
device->Release();
|
device->Release();
|
||||||
}
|
}
|
||||||
backbuf->Release();
|
backbuf->Release();
|
||||||
|
|
||||||
if (shared)
|
if (shared) {
|
||||||
{
|
|
||||||
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
|
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->publish_video_frame(bd.Width, bd.Height, static_cast<std::uint32_t>(bd.Format));
|
g_ipc->publish_video_frame(bd.Width, bd.Height, static_cast<std::uint32_t>(bd.Format));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!cant_host)
|
if (!cant_host) {
|
||||||
{
|
if (dropped && g_ipc != nullptr) {
|
||||||
if (dropped && g_ipc != nullptr)
|
|
||||||
{
|
|
||||||
g_ipc->note_video_dropped();
|
g_ipc->note_video_dropped();
|
||||||
}
|
}
|
||||||
return; // captured-or-dropped on the D3D11 path; nothing else to try this frame
|
return; // captured-or-dropped on the D3D11 path; nothing else to try this frame
|
||||||
@@ -659,8 +575,7 @@ void capture_backbuffer(IDXGISwapChain* sc)
|
|||||||
|
|
||||||
// D3D10 game: its backbuffer must be read through its own D3D10 device.
|
// D3D10 game: its backbuffer must be read through its own D3D10 device.
|
||||||
ID3D10Texture2D* bb10 = nullptr;
|
ID3D10Texture2D* bb10 = nullptr;
|
||||||
if (SUCCEEDED(sc->GetBuffer(0, __uuidof(ID3D10Texture2D), reinterpret_cast<void**>(&bb10))) && bb10 != nullptr)
|
if (SUCCEEDED(sc->GetBuffer(0, __uuidof(ID3D10Texture2D), reinterpret_cast<void**>(&bb10))) && bb10 != nullptr) {
|
||||||
{
|
|
||||||
capture_backbuffer_d3d10(sc, bb10);
|
capture_backbuffer_d3d10(sc, bb10);
|
||||||
bb10->Release();
|
bb10->Release();
|
||||||
}
|
}
|
||||||
@@ -672,8 +587,7 @@ void STDMETHODCALLTYPE hk_ExecuteCommandLists(ID3D12CommandQueue* queue, UINT nu
|
|||||||
DetourGate::Guard guard(g_gate); // keep g_present_queue/g_hk_ecl alive for this detour
|
DetourGate::Guard guard(g_gate); // keep g_present_queue/g_hk_ecl alive for this detour
|
||||||
// Record the graphics queue; compute/copy queues never present, so skip them and
|
// Record the graphics queue; compute/copy queues never present, so skip them and
|
||||||
// keep the last DIRECT one (the present queue on single-graphics-queue engines).
|
// keep the last DIRECT one (the present queue on single-graphics-queue engines).
|
||||||
if (queue != nullptr && queue->GetDesc().Type == D3D12_COMMAND_LIST_TYPE_DIRECT)
|
if (queue != nullptr && queue->GetDesc().Type == D3D12_COMMAND_LIST_TYPE_DIRECT) {
|
||||||
{
|
|
||||||
t_present_queue = queue;
|
t_present_queue = queue;
|
||||||
g_present_queue.store(queue, std::memory_order_relaxed);
|
g_present_queue.store(queue, std::memory_order_relaxed);
|
||||||
hook_note_call(g_id_ecl);
|
hook_note_call(g_id_ecl);
|
||||||
@@ -689,29 +603,25 @@ void STDMETHODCALLTYPE hk_ExecuteCommandLists(ID3D12CommandQueue* queue, UINT nu
|
|||||||
void* grab_execute_command_lists_address()
|
void* grab_execute_command_lists_address()
|
||||||
{
|
{
|
||||||
HMODULE d3d12 = GetModuleHandleW(L"d3d12.dll");
|
HMODULE d3d12 = GetModuleHandleW(L"d3d12.dll");
|
||||||
if (d3d12 == nullptr)
|
if (d3d12 == nullptr) {
|
||||||
{
|
|
||||||
return nullptr; // not a D3D12 game -> nothing to capture
|
return nullptr; // not a D3D12 game -> nothing to capture
|
||||||
}
|
}
|
||||||
using PFN_D3D12_CREATE_DEVICE = HRESULT(WINAPI*)(IUnknown*, D3D_FEATURE_LEVEL, REFIID, void**);
|
using PFN_D3D12_CREATE_DEVICE = HRESULT(WINAPI*)(IUnknown*, D3D_FEATURE_LEVEL, REFIID, void**);
|
||||||
auto create = reinterpret_cast<PFN_D3D12_CREATE_DEVICE>(GetProcAddress(d3d12, "D3D12CreateDevice"));
|
auto create = reinterpret_cast<PFN_D3D12_CREATE_DEVICE>(GetProcAddress(d3d12, "D3D12CreateDevice"));
|
||||||
if (create == nullptr)
|
if (create == nullptr) {
|
||||||
{
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
ID3D12Device* dev = nullptr;
|
ID3D12Device* dev = nullptr;
|
||||||
if (FAILED(create(nullptr, D3D_FEATURE_LEVEL_11_0, __uuidof(ID3D12Device), reinterpret_cast<void**>(&dev))) ||
|
if (FAILED(create(nullptr, D3D_FEATURE_LEVEL_11_0, __uuidof(ID3D12Device), reinterpret_cast<void**>(&dev)))
|
||||||
dev == nullptr)
|
|| dev == nullptr) {
|
||||||
{
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
D3D12_COMMAND_QUEUE_DESC qd{};
|
D3D12_COMMAND_QUEUE_DESC qd{};
|
||||||
qd.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
|
qd.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
|
||||||
ID3D12CommandQueue* queue = nullptr;
|
ID3D12CommandQueue* queue = nullptr;
|
||||||
void* addr = nullptr;
|
void* addr = nullptr;
|
||||||
if (SUCCEEDED(dev->CreateCommandQueue(&qd, __uuidof(ID3D12CommandQueue), reinterpret_cast<void**>(&queue))) &&
|
if (SUCCEEDED(dev->CreateCommandQueue(&qd, __uuidof(ID3D12CommandQueue), reinterpret_cast<void**>(&queue)))
|
||||||
queue != nullptr)
|
&& queue != nullptr) {
|
||||||
{
|
|
||||||
addr = vtable_method(queue, kIdx_ID3D12CommandQueue_ExecuteCommandLists);
|
addr = vtable_method(queue, kIdx_ID3D12CommandQueue_ExecuteCommandLists);
|
||||||
queue->Release();
|
queue->Release();
|
||||||
}
|
}
|
||||||
@@ -728,17 +638,14 @@ void on_present(IDXGISwapChain* sc, UINT flags, int hook_id, const char* method)
|
|||||||
{
|
{
|
||||||
hook_note_call(hook_id);
|
hook_note_call(hook_id);
|
||||||
g_present_calls.fetch_add(1, std::memory_order_relaxed);
|
g_present_calls.fetch_add(1, std::memory_order_relaxed);
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->note_present();
|
g_ipc->note_present();
|
||||||
}
|
}
|
||||||
if (first_present_with_flags(sc, flags))
|
if (first_present_with_flags(sc, flags)) {
|
||||||
{
|
|
||||||
logf("present: swapchain=%p %s flags=0x%08X%s", sc, method, flags,
|
logf("present: swapchain=%p %s flags=0x%08X%s", sc, method, flags,
|
||||||
(flags & DXGI_PRESENT_TEST) ? " (DXGI_PRESENT_TEST: occlusion probe, no frame drawn)" : "");
|
(flags & DXGI_PRESENT_TEST) ? " (DXGI_PRESENT_TEST: occlusion probe, no frame drawn)" : "");
|
||||||
}
|
}
|
||||||
if ((flags & DXGI_PRESENT_TEST) == 0)
|
if ((flags & DXGI_PRESENT_TEST) == 0) {
|
||||||
{
|
|
||||||
capture_backbuffer(sc);
|
capture_backbuffer(sc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -779,8 +686,7 @@ void* grab_present_address(void** present1_out)
|
|||||||
RegisterClassExW(&wc);
|
RegisterClassExW(&wc);
|
||||||
HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"", WS_OVERLAPPEDWINDOW, 0, 0, 8, 8, nullptr, nullptr,
|
HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"", WS_OVERLAPPEDWINDOW, 0, 0, 8, 8, nullptr, nullptr,
|
||||||
wc.hInstance, nullptr);
|
wc.hInstance, nullptr);
|
||||||
if (hwnd == nullptr)
|
if (hwnd == nullptr) {
|
||||||
{
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,31 +707,24 @@ void* grab_present_address(void** present1_out)
|
|||||||
const HRESULT hr = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0,
|
const HRESULT hr = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0,
|
||||||
D3D11_SDK_VERSION, &scd, &swapchain, &device, nullptr, &ctx);
|
D3D11_SDK_VERSION, &scd, &swapchain, &device, nullptr, &ctx);
|
||||||
void* present = nullptr;
|
void* present = nullptr;
|
||||||
if (SUCCEEDED(hr) && swapchain != nullptr)
|
if (SUCCEEDED(hr) && swapchain != nullptr) {
|
||||||
{
|
|
||||||
present = vtable_method(swapchain, kIdx_IDXGISwapChain_Present);
|
present = vtable_method(swapchain, kIdx_IDXGISwapChain_Present);
|
||||||
IDXGISwapChain1* sc1 = nullptr;
|
IDXGISwapChain1* sc1 = nullptr;
|
||||||
if (SUCCEEDED(swapchain->QueryInterface(__uuidof(IDXGISwapChain1), reinterpret_cast<void**>(&sc1))) &&
|
if (SUCCEEDED(swapchain->QueryInterface(__uuidof(IDXGISwapChain1), reinterpret_cast<void**>(&sc1)))
|
||||||
sc1 != nullptr)
|
&& sc1 != nullptr) {
|
||||||
{
|
|
||||||
*present1_out = vtable_method(sc1, kIdx_IDXGISwapChain1_Present1);
|
*present1_out = vtable_method(sc1, kIdx_IDXGISwapChain1_Present1);
|
||||||
sc1->Release();
|
sc1->Release();
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
logf("present: D3D11CreateDeviceAndSwapChain(probe) failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
logf("present: D3D11CreateDeviceAndSwapChain(probe) failed hr=0x%08lX", static_cast<unsigned long>(hr));
|
||||||
}
|
}
|
||||||
if (ctx != nullptr)
|
if (ctx != nullptr) {
|
||||||
{
|
|
||||||
ctx->Release();
|
ctx->Release();
|
||||||
}
|
}
|
||||||
if (device != nullptr)
|
if (device != nullptr) {
|
||||||
{
|
|
||||||
device->Release();
|
device->Release();
|
||||||
}
|
}
|
||||||
if (swapchain != nullptr)
|
if (swapchain != nullptr) {
|
||||||
{
|
|
||||||
swapchain->Release();
|
swapchain->Release();
|
||||||
}
|
}
|
||||||
DestroyWindow(hwnd);
|
DestroyWindow(hwnd);
|
||||||
@@ -839,8 +738,7 @@ bool install_present_hooks(IpcClient& ipc)
|
|||||||
{
|
{
|
||||||
g_ipc = &ipc;
|
g_ipc = &ipc;
|
||||||
g_pid = GetCurrentProcessId();
|
g_pid = GetCurrentProcessId();
|
||||||
if (g_hk_present.enabled())
|
if (g_hk_present.enabled()) {
|
||||||
{
|
|
||||||
return true; // already installed (persistent hook; the re-install path below re-enables it)
|
return true; // already installed (persistent hook; the re-install path below re-enables it)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -850,15 +748,13 @@ bool install_present_hooks(IpcClient& ipc)
|
|||||||
|
|
||||||
void* present1 = nullptr;
|
void* present1 = nullptr;
|
||||||
void* present = grab_present_address(&present1);
|
void* present = grab_present_address(&present1);
|
||||||
if (present == nullptr)
|
if (present == nullptr) {
|
||||||
{
|
|
||||||
hook_set_installed(g_id_present, false);
|
hook_set_installed(g_id_present, false);
|
||||||
hook_set_installed(g_id_present1, false);
|
hook_set_installed(g_id_present1, false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
install_inline(g_hk_present, present, &hk_Present);
|
install_inline(g_hk_present, present, &hk_Present);
|
||||||
if (present1 != nullptr)
|
if (present1 != nullptr) {
|
||||||
{
|
|
||||||
install_inline(g_hk_present1, present1, &hk_Present1);
|
install_inline(g_hk_present1, present1, &hk_Present1);
|
||||||
}
|
}
|
||||||
g_unsupported_logged = false;
|
g_unsupported_logged = false;
|
||||||
@@ -871,15 +767,11 @@ bool install_present_hooks(IpcClient& ipc)
|
|||||||
// here at injection time -- d3d12.dll is already loaded in a running D3D12 game --
|
// here at injection time -- d3d12.dll is already loaded in a running D3D12 game --
|
||||||
// so the queue is recovered even though we attached after it was created.
|
// so the queue is recovered even though we attached after it was created.
|
||||||
void* ecl = grab_execute_command_lists_address();
|
void* ecl = grab_execute_command_lists_address();
|
||||||
if (ecl != nullptr)
|
if (ecl != nullptr) {
|
||||||
{
|
|
||||||
install_inline(g_hk_ecl, ecl, &hk_ExecuteCommandLists);
|
install_inline(g_hk_ecl, ecl, &hk_ExecuteCommandLists);
|
||||||
hook_set_installed(g_id_ecl, static_cast<bool>(g_hk_ecl));
|
hook_set_installed(g_id_ecl, static_cast<bool>(g_hk_ecl));
|
||||||
logf("install_present_hooks: d3d12 ExecuteCommandLists=%p hooked=%d", ecl,
|
logf("install_present_hooks: d3d12 ExecuteCommandLists=%p hooked=%d", ecl, static_cast<bool>(g_hk_ecl) ? 1 : 0);
|
||||||
static_cast<bool>(g_hk_ecl) ? 1 : 0);
|
} else {
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
hook_set_installed(g_id_ecl, false); // not a D3D12 game; On12 path uses its own queue
|
hook_set_installed(g_id_ecl, false); // not a D3D12 game; On12 path uses its own queue
|
||||||
}
|
}
|
||||||
return static_cast<bool>(g_hk_present);
|
return static_cast<bool>(g_hk_present);
|
||||||
|
|||||||
@@ -13,8 +13,7 @@
|
|||||||
|
|
||||||
#include "ipc_client.hpp"
|
#include "ipc_client.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
// Installs the Present hook. Grabs IDXGISwapChain::Present from a throwaway
|
// Installs the Present hook. Grabs IDXGISwapChain::Present from a throwaway
|
||||||
// swapchain and inline-hooks it, so every swapchain in the process is caught.
|
// swapchain and inline-hooks it, so every swapchain in the process is caught.
|
||||||
|
|||||||
@@ -25,8 +25,7 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
// Snap a measured rate to the nearest standard rate when within `tol` (fractional);
|
// Snap a measured rate to the nearest standard rate when within `tol` (fractional);
|
||||||
// returns 0 when it doesn't land near any standard rate. The standard rates are spaced
|
// returns 0 when it doesn't land near any standard rate. The standard rates are spaced
|
||||||
@@ -35,10 +34,8 @@ inline std::uint32_t snap_standard_rate(double measured, double tol = 0.02)
|
|||||||
{
|
{
|
||||||
static constexpr std::uint32_t kStd[] = {8000, 11025, 16000, 22050, 32000, 44100,
|
static constexpr std::uint32_t kStd[] = {8000, 11025, 16000, 22050, 32000, 44100,
|
||||||
48000, 88200, 96000, 176400, 192000};
|
48000, 88200, 96000, 176400, 192000};
|
||||||
for (std::uint32_t s : kStd)
|
for (std::uint32_t s : kStd) {
|
||||||
{
|
if (measured >= s * (1.0 - tol) && measured <= s * (1.0 + tol)) {
|
||||||
if (measured >= s * (1.0 - tol) && measured <= s * (1.0 + tol))
|
|
||||||
{
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,15 +43,13 @@ inline std::uint32_t snap_standard_rate(double measured, double tol = 0.02)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Outcome of feeding one measurement tick.
|
// Outcome of feeding one measurement tick.
|
||||||
struct RateEstimate
|
struct RateEstimate {
|
||||||
{
|
|
||||||
bool done = false; // a rate has been decided (stop feeding)
|
bool done = false; // a rate has been decided (stop feeding)
|
||||||
std::uint32_t rate = 0; // the decided rate, valid when done
|
std::uint32_t rate = 0; // the decided rate, valid when done
|
||||||
bool confident = false; // true = consensus on a standard rate; false = low-confidence fallback
|
bool confident = false; // true = consensus on a standard rate; false = low-confidence fallback
|
||||||
};
|
};
|
||||||
|
|
||||||
class RateEstimator
|
class RateEstimator {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
// Window length, consensus count, and the attempt budget before giving up to a
|
// Window length, consensus count, and the attempt budget before giving up to a
|
||||||
// low-confidence estimate. Public so a caller/test can tune them; the defaults are
|
// low-confidence estimate. Public so a caller/test can tune them; the defaults are
|
||||||
@@ -68,18 +63,15 @@ public:
|
|||||||
// Call repeatedly (e.g. each worker tick); returns done=false while still measuring.
|
// Call repeatedly (e.g. each worker tick); returns done=false while still measuring.
|
||||||
RateEstimate feed(std::uint64_t frames, std::int64_t now_qpc, std::int64_t freq)
|
RateEstimate feed(std::uint64_t frames, std::int64_t now_qpc, std::int64_t freq)
|
||||||
{
|
{
|
||||||
if (freq <= 0)
|
if (freq <= 0) {
|
||||||
{
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
if (window_qpc_ == 0)
|
if (window_qpc_ == 0) {
|
||||||
{
|
|
||||||
start_window(frames, now_qpc); // begin the first window
|
start_window(frames, now_qpc); // begin the first window
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const std::int64_t dt = now_qpc - window_qpc_;
|
const std::int64_t dt = now_qpc - window_qpc_;
|
||||||
if (dt < static_cast<std::int64_t>(window_seconds * static_cast<double>(freq)))
|
if (dt < static_cast<std::int64_t>(window_seconds * static_cast<double>(freq))) {
|
||||||
{
|
|
||||||
return {}; // window still filling
|
return {}; // window still filling
|
||||||
}
|
}
|
||||||
const std::uint64_t df = frames - window_frames_;
|
const std::uint64_t df = frames - window_frames_;
|
||||||
@@ -87,16 +79,14 @@ public:
|
|||||||
start_window(frames, now_qpc); // next window starts here
|
start_window(frames, now_qpc); // next window starts here
|
||||||
const double raw = static_cast<double>(df) / secs;
|
const double raw = static_cast<double>(df) / secs;
|
||||||
|
|
||||||
if (raw < min_audio_rate)
|
if (raw < min_audio_rate) {
|
||||||
{
|
|
||||||
// Stream went (near-)idle this window: can't trust it. Drop back to the
|
// Stream went (near-)idle this window: can't trust it. Drop back to the
|
||||||
// warm-up state so the next active window is discarded, not measured.
|
// warm-up state so the next active window is discarded, not measured.
|
||||||
primed_ = false;
|
primed_ = false;
|
||||||
reset_consensus();
|
reset_consensus();
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
if (!primed_)
|
if (!primed_) {
|
||||||
{
|
|
||||||
// Discard the first full active window: a freshly-attached stream can deliver
|
// Discard the first full active window: a freshly-attached stream can deliver
|
||||||
// its already-queued buffers in a burst, over-counting frames.
|
// its already-queued buffers in a burst, over-counting frames.
|
||||||
primed_ = true;
|
primed_ = true;
|
||||||
@@ -107,32 +97,23 @@ public:
|
|||||||
++attempts_;
|
++attempts_;
|
||||||
last_raw_ = raw;
|
last_raw_ = raw;
|
||||||
const std::uint32_t snapped = snap_standard_rate(raw);
|
const std::uint32_t snapped = snap_standard_rate(raw);
|
||||||
if (snapped != 0)
|
if (snapped != 0) {
|
||||||
{
|
if (snapped == last_snapped_) {
|
||||||
if (snapped == last_snapped_)
|
|
||||||
{
|
|
||||||
++agree_;
|
++agree_;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
last_snapped_ = snapped;
|
last_snapped_ = snapped;
|
||||||
agree_ = 1;
|
agree_ = 1;
|
||||||
}
|
}
|
||||||
if (agree_ >= needed_agree)
|
if (agree_ >= needed_agree) {
|
||||||
{
|
|
||||||
return {true, snapped, true}; // consensus -> confident
|
return {true, snapped, true}; // consensus -> confident
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
reset_consensus(); // a non-snapping window breaks the streak
|
reset_consensus(); // a non-snapping window breaks the streak
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempts_ >= max_attempts)
|
if (attempts_ >= max_attempts) {
|
||||||
{
|
|
||||||
// Give up on consensus: a snapped value seen along the way beats a raw one.
|
// Give up on consensus: a snapped value seen along the way beats a raw one.
|
||||||
const std::uint32_t best =
|
const std::uint32_t best = last_snapped_ != 0 ? last_snapped_ : static_cast<std::uint32_t>(last_raw_ + 0.5);
|
||||||
last_snapped_ != 0 ? last_snapped_ : static_cast<std::uint32_t>(last_raw_ + 0.5);
|
|
||||||
return {true, best, false}; // low-confidence
|
return {true, best, false}; // low-confidence
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -13,17 +13,12 @@
|
|||||||
#include "coop/shared_memory.hpp"
|
#include "coop/shared_memory.hpp"
|
||||||
#include "debug_log.hpp"
|
#include "debug_log.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
class SharedVideoTexture
|
class SharedVideoTexture {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
SharedVideoTexture() = default;
|
SharedVideoTexture() = default;
|
||||||
~SharedVideoTexture()
|
~SharedVideoTexture() { release(); }
|
||||||
{
|
|
||||||
release();
|
|
||||||
}
|
|
||||||
|
|
||||||
SharedVideoTexture(const SharedVideoTexture&) = delete;
|
SharedVideoTexture(const SharedVideoTexture&) = delete;
|
||||||
SharedVideoTexture& operator=(const SharedVideoTexture&) = delete;
|
SharedVideoTexture& operator=(const SharedVideoTexture&) = delete;
|
||||||
@@ -35,8 +30,7 @@ public:
|
|||||||
bool ensure(ID3D11Device* device, UINT w, UINT h, DXGI_FORMAT fmt, unsigned long pid, const char* tag,
|
bool ensure(ID3D11Device* device, UINT w, UINT h, DXGI_FORMAT fmt, unsigned long pid, const char* tag,
|
||||||
UINT bind = D3D11_BIND_SHADER_RESOURCE)
|
UINT bind = D3D11_BIND_SHADER_RESOURCE)
|
||||||
{
|
{
|
||||||
if (m_tex != nullptr && m_w == w && m_h == h && m_fmt == fmt)
|
if (m_tex != nullptr && m_w == w && m_h == h && m_fmt == fmt) {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
release();
|
release();
|
||||||
@@ -52,35 +46,31 @@ public:
|
|||||||
desc.BindFlags = bind;
|
desc.BindFlags = bind;
|
||||||
desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;
|
desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;
|
||||||
HRESULT hr = device->CreateTexture2D(&desc, nullptr, &m_tex);
|
HRESULT hr = device->CreateTexture2D(&desc, nullptr, &m_tex);
|
||||||
if (FAILED(hr) || m_tex == nullptr)
|
if (FAILED(hr) || m_tex == nullptr) {
|
||||||
{
|
logf("%s: CreateTexture2D(shared) failed hr=0x%08lX (%ux%u fmt=%d)", tag, static_cast<unsigned long>(hr), w,
|
||||||
logf("%s: CreateTexture2D(shared) failed hr=0x%08lX (%ux%u fmt=%d)", tag,
|
h, static_cast<int>(fmt));
|
||||||
static_cast<unsigned long>(hr), w, h, static_cast<int>(fmt));
|
|
||||||
release();
|
release();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
IDXGIResource1* res = nullptr;
|
IDXGIResource1* res = nullptr;
|
||||||
hr = m_tex->QueryInterface(__uuidof(IDXGIResource1), reinterpret_cast<void**>(&res));
|
hr = m_tex->QueryInterface(__uuidof(IDXGIResource1), reinterpret_cast<void**>(&res));
|
||||||
if (FAILED(hr) || res == nullptr)
|
if (FAILED(hr) || res == nullptr) {
|
||||||
{
|
|
||||||
logf("%s: QI IDXGIResource1 failed hr=0x%08lX", tag, static_cast<unsigned long>(hr));
|
logf("%s: QI IDXGIResource1 failed hr=0x%08lX", tag, static_cast<unsigned long>(hr));
|
||||||
release();
|
release();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const std::wstring name = video_share_name(pid);
|
const std::wstring name = video_share_name(pid);
|
||||||
hr = res->CreateSharedHandle(nullptr, DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE,
|
hr = res->CreateSharedHandle(nullptr, DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, name.c_str(),
|
||||||
name.c_str(), &m_handle);
|
&m_handle);
|
||||||
res->Release();
|
res->Release();
|
||||||
if (FAILED(hr) || m_handle == nullptr)
|
if (FAILED(hr) || m_handle == nullptr) {
|
||||||
{
|
|
||||||
logf("%s: CreateSharedHandle failed hr=0x%08lX", tag, static_cast<unsigned long>(hr));
|
logf("%s: CreateSharedHandle failed hr=0x%08lX", tag, static_cast<unsigned long>(hr));
|
||||||
release();
|
release();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
hr = m_tex->QueryInterface(__uuidof(IDXGIKeyedMutex), reinterpret_cast<void**>(&m_mutex));
|
hr = m_tex->QueryInterface(__uuidof(IDXGIKeyedMutex), reinterpret_cast<void**>(&m_mutex));
|
||||||
if (FAILED(hr) || m_mutex == nullptr)
|
if (FAILED(hr) || m_mutex == nullptr) {
|
||||||
{
|
|
||||||
logf("%s: QI IDXGIKeyedMutex failed hr=0x%08lX", tag, static_cast<unsigned long>(hr));
|
logf("%s: QI IDXGIKeyedMutex failed hr=0x%08lX", tag, static_cast<unsigned long>(hr));
|
||||||
release();
|
release();
|
||||||
return false;
|
return false;
|
||||||
@@ -95,18 +85,15 @@ public:
|
|||||||
|
|
||||||
void release()
|
void release()
|
||||||
{
|
{
|
||||||
if (m_mutex != nullptr)
|
if (m_mutex != nullptr) {
|
||||||
{
|
|
||||||
m_mutex->Release();
|
m_mutex->Release();
|
||||||
m_mutex = nullptr;
|
m_mutex = nullptr;
|
||||||
}
|
}
|
||||||
if (m_tex != nullptr)
|
if (m_tex != nullptr) {
|
||||||
{
|
|
||||||
m_tex->Release();
|
m_tex->Release();
|
||||||
m_tex = nullptr;
|
m_tex = nullptr;
|
||||||
}
|
}
|
||||||
if (m_handle != nullptr)
|
if (m_handle != nullptr) {
|
||||||
{
|
|
||||||
CloseHandle(m_handle);
|
CloseHandle(m_handle);
|
||||||
m_handle = nullptr;
|
m_handle = nullptr;
|
||||||
}
|
}
|
||||||
@@ -114,14 +101,8 @@ public:
|
|||||||
m_fmt = DXGI_FORMAT_UNKNOWN;
|
m_fmt = DXGI_FORMAT_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] ID3D11Texture2D* texture() const
|
[[nodiscard]] ID3D11Texture2D* texture() const { return m_tex; }
|
||||||
{
|
[[nodiscard]] IDXGIKeyedMutex* mutex() const { return m_mutex; }
|
||||||
return m_tex;
|
|
||||||
}
|
|
||||||
[[nodiscard]] IDXGIKeyedMutex* mutex() const
|
|
||||||
{
|
|
||||||
return m_mutex;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ID3D11Texture2D* m_tex = nullptr;
|
ID3D11Texture2D* m_tex = nullptr;
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
|
|
||||||
#include "coop/protocol.hpp"
|
#include "coop/protocol.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
VkCapture::~VkCapture()
|
VkCapture::~VkCapture()
|
||||||
{
|
{
|
||||||
@@ -27,43 +26,31 @@ bool VkCapture::find_readback_memory(std::uint32_t type_bits, std::uint32_t& out
|
|||||||
int best = -1;
|
int best = -1;
|
||||||
bool best_coherent = true;
|
bool best_coherent = true;
|
||||||
int best_rank = -1;
|
int best_rank = -1;
|
||||||
for (std::uint32_t i = 0; i < mp.memoryTypeCount; ++i)
|
for (std::uint32_t i = 0; i < mp.memoryTypeCount; ++i) {
|
||||||
{
|
if ((type_bits & (1u << i)) == 0) {
|
||||||
if ((type_bits & (1u << i)) == 0)
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const VkMemoryPropertyFlags f = mp.memoryTypes[i].propertyFlags;
|
const VkMemoryPropertyFlags f = mp.memoryTypes[i].propertyFlags;
|
||||||
if ((f & vis) == 0)
|
if ((f & vis) == 0) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int rank;
|
int rank;
|
||||||
if ((f & cached) && (f & coherent))
|
if ((f & cached) && (f & coherent)) {
|
||||||
{
|
|
||||||
rank = 3;
|
rank = 3;
|
||||||
}
|
} else if (f & cached) {
|
||||||
else if (f & cached)
|
|
||||||
{
|
|
||||||
rank = 2;
|
rank = 2;
|
||||||
}
|
} else if (f & coherent) {
|
||||||
else if (f & coherent)
|
|
||||||
{
|
|
||||||
rank = 1;
|
rank = 1;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
continue; // host-visible but neither cached nor coherent: unusable for a CPU read-back
|
continue; // host-visible but neither cached nor coherent: unusable for a CPU read-back
|
||||||
}
|
}
|
||||||
if (rank > best_rank)
|
if (rank > best_rank) {
|
||||||
{
|
|
||||||
best_rank = rank;
|
best_rank = rank;
|
||||||
best = static_cast<int>(i);
|
best = static_cast<int>(i);
|
||||||
best_coherent = (f & coherent) != 0;
|
best_coherent = (f & coherent) != 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (best < 0)
|
if (best < 0) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
out_index = static_cast<std::uint32_t>(best);
|
out_index = static_cast<std::uint32_t>(best);
|
||||||
@@ -73,33 +60,28 @@ bool VkCapture::find_readback_memory(std::uint32_t type_bits, std::uint32_t& out
|
|||||||
|
|
||||||
bool VkCapture::ensure_slot_pool()
|
bool VkCapture::ensure_slot_pool()
|
||||||
{
|
{
|
||||||
if (m_pool != VK_NULL_HANDLE)
|
if (m_pool != VK_NULL_HANDLE) {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (m_queue == VK_NULL_HANDLE)
|
if (m_queue == VK_NULL_HANDLE) {
|
||||||
{
|
|
||||||
m_fns.GetDeviceQueue(m_device, m_qfam, 0, &m_queue);
|
m_fns.GetDeviceQueue(m_device, m_qfam, 0, &m_queue);
|
||||||
}
|
}
|
||||||
VkCommandPoolCreateInfo pci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
VkCommandPoolCreateInfo pci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||||
pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||||
pci.queueFamilyIndex = m_qfam;
|
pci.queueFamilyIndex = m_qfam;
|
||||||
if (m_fns.CreateCommandPool(m_device, &pci, nullptr, &m_pool) != VK_SUCCESS)
|
if (m_fns.CreateCommandPool(m_device, &pci, nullptr, &m_pool) != VK_SUCCESS) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (Slot& s : m_slots)
|
for (Slot& s : m_slots) {
|
||||||
{
|
|
||||||
VkCommandBufferAllocateInfo ai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
VkCommandBufferAllocateInfo ai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
||||||
ai.commandPool = m_pool;
|
ai.commandPool = m_pool;
|
||||||
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||||
ai.commandBufferCount = 1;
|
ai.commandBufferCount = 1;
|
||||||
VkFenceCreateInfo fi{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
VkFenceCreateInfo fi{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
||||||
VkSemaphoreCreateInfo si{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
VkSemaphoreCreateInfo si{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
||||||
if (m_fns.AllocateCommandBuffers(m_device, &ai, &s.cmd) != VK_SUCCESS ||
|
if (m_fns.AllocateCommandBuffers(m_device, &ai, &s.cmd) != VK_SUCCESS
|
||||||
m_fns.CreateFence(m_device, &fi, nullptr, &s.fence) != VK_SUCCESS ||
|
|| m_fns.CreateFence(m_device, &fi, nullptr, &s.fence) != VK_SUCCESS
|
||||||
m_fns.CreateSemaphore(m_device, &si, nullptr, &s.present_sem) != VK_SUCCESS)
|
|| m_fns.CreateSemaphore(m_device, &si, nullptr, &s.present_sem) != VK_SUCCESS) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,22 +91,18 @@ bool VkCapture::ensure_slot_pool()
|
|||||||
bool VkCapture::ensure_staging(Slot& s, std::uint32_t w, std::uint32_t h)
|
bool VkCapture::ensure_staging(Slot& s, std::uint32_t w, std::uint32_t h)
|
||||||
{
|
{
|
||||||
const VkDeviceSize need = static_cast<VkDeviceSize>(w) * h * 4;
|
const VkDeviceSize need = static_cast<VkDeviceSize>(w) * h * 4;
|
||||||
if (s.staging != VK_NULL_HANDLE && s.size == need)
|
if (s.staging != VK_NULL_HANDLE && s.size == need) {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (s.mapped != nullptr)
|
if (s.mapped != nullptr) {
|
||||||
{
|
|
||||||
m_fns.UnmapMemory(m_device, s.mem);
|
m_fns.UnmapMemory(m_device, s.mem);
|
||||||
s.mapped = nullptr;
|
s.mapped = nullptr;
|
||||||
}
|
}
|
||||||
if (s.staging != VK_NULL_HANDLE)
|
if (s.staging != VK_NULL_HANDLE) {
|
||||||
{
|
|
||||||
m_fns.DestroyBuffer(m_device, s.staging, nullptr);
|
m_fns.DestroyBuffer(m_device, s.staging, nullptr);
|
||||||
s.staging = VK_NULL_HANDLE;
|
s.staging = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
if (s.mem != VK_NULL_HANDLE)
|
if (s.mem != VK_NULL_HANDLE) {
|
||||||
{
|
|
||||||
m_fns.FreeMemory(m_device, s.mem, nullptr);
|
m_fns.FreeMemory(m_device, s.mem, nullptr);
|
||||||
s.mem = VK_NULL_HANDLE;
|
s.mem = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
@@ -133,16 +111,14 @@ bool VkCapture::ensure_staging(Slot& s, std::uint32_t w, std::uint32_t h)
|
|||||||
bci.size = need;
|
bci.size = need;
|
||||||
bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||||
bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||||
if (m_fns.CreateBuffer(m_device, &bci, nullptr, &s.staging) != VK_SUCCESS)
|
if (m_fns.CreateBuffer(m_device, &bci, nullptr, &s.staging) != VK_SUCCESS) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
VkMemoryRequirements mr{};
|
VkMemoryRequirements mr{};
|
||||||
m_fns.GetBufferMemoryRequirements(m_device, s.staging, &mr);
|
m_fns.GetBufferMemoryRequirements(m_device, s.staging, &mr);
|
||||||
std::uint32_t mt = 0;
|
std::uint32_t mt = 0;
|
||||||
bool coherent = true;
|
bool coherent = true;
|
||||||
if (!find_readback_memory(mr.memoryTypeBits, mt, coherent))
|
if (!find_readback_memory(mr.memoryTypeBits, mt, coherent)) {
|
||||||
{
|
|
||||||
m_fns.DestroyBuffer(m_device, s.staging, nullptr);
|
m_fns.DestroyBuffer(m_device, s.staging, nullptr);
|
||||||
s.staging = VK_NULL_HANDLE;
|
s.staging = VK_NULL_HANDLE;
|
||||||
return false;
|
return false;
|
||||||
@@ -150,12 +126,10 @@ bool VkCapture::ensure_staging(Slot& s, std::uint32_t w, std::uint32_t h)
|
|||||||
VkMemoryAllocateInfo mai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
VkMemoryAllocateInfo mai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||||
mai.allocationSize = mr.size;
|
mai.allocationSize = mr.size;
|
||||||
mai.memoryTypeIndex = mt;
|
mai.memoryTypeIndex = mt;
|
||||||
if (m_fns.AllocateMemory(m_device, &mai, nullptr, &s.mem) != VK_SUCCESS ||
|
if (m_fns.AllocateMemory(m_device, &mai, nullptr, &s.mem) != VK_SUCCESS
|
||||||
m_fns.BindBufferMemory(m_device, s.staging, s.mem, 0) != VK_SUCCESS ||
|
|| m_fns.BindBufferMemory(m_device, s.staging, s.mem, 0) != VK_SUCCESS
|
||||||
m_fns.MapMemory(m_device, s.mem, 0, VK_WHOLE_SIZE, 0, &s.mapped) != VK_SUCCESS)
|
|| m_fns.MapMemory(m_device, s.mem, 0, VK_WHOLE_SIZE, 0, &s.mapped) != VK_SUCCESS) {
|
||||||
{
|
if (s.mem != VK_NULL_HANDLE) {
|
||||||
if (s.mem != VK_NULL_HANDLE)
|
|
||||||
{
|
|
||||||
m_fns.FreeMemory(m_device, s.mem, nullptr);
|
m_fns.FreeMemory(m_device, s.mem, nullptr);
|
||||||
s.mem = VK_NULL_HANDLE;
|
s.mem = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
@@ -170,38 +144,31 @@ bool VkCapture::ensure_staging(Slot& s, std::uint32_t w, std::uint32_t h)
|
|||||||
|
|
||||||
void VkCapture::free_slots()
|
void VkCapture::free_slots()
|
||||||
{
|
{
|
||||||
for (Slot& s : m_slots)
|
for (Slot& s : m_slots) {
|
||||||
{
|
if (s.mapped != nullptr) {
|
||||||
if (s.mapped != nullptr)
|
|
||||||
{
|
|
||||||
m_fns.UnmapMemory(m_device, s.mem);
|
m_fns.UnmapMemory(m_device, s.mem);
|
||||||
s.mapped = nullptr;
|
s.mapped = nullptr;
|
||||||
}
|
}
|
||||||
if (s.staging != VK_NULL_HANDLE)
|
if (s.staging != VK_NULL_HANDLE) {
|
||||||
{
|
|
||||||
m_fns.DestroyBuffer(m_device, s.staging, nullptr);
|
m_fns.DestroyBuffer(m_device, s.staging, nullptr);
|
||||||
s.staging = VK_NULL_HANDLE;
|
s.staging = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
if (s.mem != VK_NULL_HANDLE)
|
if (s.mem != VK_NULL_HANDLE) {
|
||||||
{
|
|
||||||
m_fns.FreeMemory(m_device, s.mem, nullptr);
|
m_fns.FreeMemory(m_device, s.mem, nullptr);
|
||||||
s.mem = VK_NULL_HANDLE;
|
s.mem = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
if (s.present_sem != VK_NULL_HANDLE)
|
if (s.present_sem != VK_NULL_HANDLE) {
|
||||||
{
|
|
||||||
m_fns.DestroySemaphore(m_device, s.present_sem, nullptr);
|
m_fns.DestroySemaphore(m_device, s.present_sem, nullptr);
|
||||||
s.present_sem = VK_NULL_HANDLE;
|
s.present_sem = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
if (s.fence != VK_NULL_HANDLE)
|
if (s.fence != VK_NULL_HANDLE) {
|
||||||
{
|
|
||||||
m_fns.DestroyFence(m_device, s.fence, nullptr);
|
m_fns.DestroyFence(m_device, s.fence, nullptr);
|
||||||
s.fence = VK_NULL_HANDLE;
|
s.fence = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
s.size = 0;
|
s.size = 0;
|
||||||
s.busy.store(false, std::memory_order_relaxed);
|
s.busy.store(false, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
if (m_pool != VK_NULL_HANDLE)
|
if (m_pool != VK_NULL_HANDLE) {
|
||||||
{
|
|
||||||
m_fns.DestroyCommandPool(m_device, m_pool, nullptr); // frees the command buffers
|
m_fns.DestroyCommandPool(m_device, m_pool, nullptr); // frees the command buffers
|
||||||
m_pool = VK_NULL_HANDLE;
|
m_pool = VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
@@ -210,25 +177,22 @@ void VkCapture::free_slots()
|
|||||||
// --- D3D11 shared texture (reaper thread only; shutdown releases after the reaper has joined) ------
|
// --- D3D11 shared texture (reaper thread only; shutdown releases after the reaper has joined) ------
|
||||||
bool VkCapture::ensure_d3d()
|
bool VkCapture::ensure_d3d()
|
||||||
{
|
{
|
||||||
if (m_d3d != nullptr)
|
if (m_d3d != nullptr) {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return SUCCEEDED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0,
|
return SUCCEEDED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION,
|
||||||
D3D11_SDK_VERSION, &m_d3d, nullptr, &m_d3d_ctx)) &&
|
&m_d3d, nullptr, &m_d3d_ctx))
|
||||||
m_d3d != nullptr;
|
&& m_d3d != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkCapture::release_d3d()
|
void VkCapture::release_d3d()
|
||||||
{
|
{
|
||||||
m_shared.release();
|
m_shared.release();
|
||||||
if (m_d3d_ctx != nullptr)
|
if (m_d3d_ctx != nullptr) {
|
||||||
{
|
|
||||||
m_d3d_ctx->Release();
|
m_d3d_ctx->Release();
|
||||||
m_d3d_ctx = nullptr;
|
m_d3d_ctx = nullptr;
|
||||||
}
|
}
|
||||||
if (m_d3d != nullptr)
|
if (m_d3d != nullptr) {
|
||||||
{
|
|
||||||
m_d3d->Release();
|
m_d3d->Release();
|
||||||
m_d3d = nullptr;
|
m_d3d = nullptr;
|
||||||
}
|
}
|
||||||
@@ -238,8 +202,7 @@ void VkCapture::release_d3d()
|
|||||||
void VkCapture::init(VkPhysicalDevice phys, VkDevice device, std::uint32_t queue_family, const Fns& fns,
|
void VkCapture::init(VkPhysicalDevice phys, VkDevice device, std::uint32_t queue_family, const Fns& fns,
|
||||||
unsigned long pid, std::function<void(std::uint32_t, std::uint32_t)> on_frame)
|
unsigned long pid, std::function<void(std::uint32_t, std::uint32_t)> on_frame)
|
||||||
{
|
{
|
||||||
if (m_device != VK_NULL_HANDLE)
|
if (m_device != VK_NULL_HANDLE) {
|
||||||
{
|
|
||||||
return; // already initialised
|
return; // already initialised
|
||||||
}
|
}
|
||||||
m_phys = phys;
|
m_phys = phys;
|
||||||
@@ -249,8 +212,7 @@ void VkCapture::init(VkPhysicalDevice phys, VkDevice device, std::uint32_t queue
|
|||||||
m_pid = pid;
|
m_pid = pid;
|
||||||
m_on_frame = std::move(on_frame);
|
m_on_frame = std::move(on_frame);
|
||||||
m_stop = false;
|
m_stop = false;
|
||||||
if (!ensure_slot_pool())
|
if (!ensure_slot_pool()) {
|
||||||
{
|
|
||||||
return; // leave m_device set but the pool empty -> present() will fail format/staging checks
|
return; // leave m_device set but the pool empty -> present() will fail format/staging checks
|
||||||
}
|
}
|
||||||
m_reaper = std::thread([this] { reaper_main(); });
|
m_reaper = std::thread([this] { reaper_main(); });
|
||||||
@@ -261,8 +223,7 @@ bool VkCapture::present(VkImage image, VkFormat fmt, std::uint32_t w, std::uint3
|
|||||||
{
|
{
|
||||||
const bool bgra = fmt == VK_FORMAT_B8G8R8A8_UNORM || fmt == VK_FORMAT_B8G8R8A8_SRGB;
|
const bool bgra = fmt == VK_FORMAT_B8G8R8A8_UNORM || fmt == VK_FORMAT_B8G8R8A8_SRGB;
|
||||||
const bool rgba = fmt == VK_FORMAT_R8G8B8A8_UNORM || fmt == VK_FORMAT_R8G8B8A8_SRGB;
|
const bool rgba = fmt == VK_FORMAT_R8G8B8A8_UNORM || fmt == VK_FORMAT_R8G8B8A8_SRGB;
|
||||||
if (m_device == VK_NULL_HANDLE || m_pool == VK_NULL_HANDLE || (!bgra && !rgba))
|
if (m_device == VK_NULL_HANDLE || m_pool == VK_NULL_HANDLE || (!bgra && !rgba)) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// No time-based throttle here: capture follows the game's present rate, which vsync paces (if the
|
// No time-based throttle here: capture follows the game's present rate, which vsync paces (if the
|
||||||
@@ -272,23 +233,19 @@ bool VkCapture::present(VkImage image, VkFormat fmt, std::uint32_t w, std::uint3
|
|||||||
// Pick a slot whose previous capture the reaper has finished. None free -> the reaper is behind,
|
// Pick a slot whose previous capture the reaper has finished. None free -> the reaper is behind,
|
||||||
// so skip this frame (the game keeps its rate; the mirror just drops a frame).
|
// so skip this frame (the game keeps its rate; the mirror just drops a frame).
|
||||||
int idx = -1;
|
int idx = -1;
|
||||||
for (int n = 0; n < kSlots; ++n)
|
for (int n = 0; n < kSlots; ++n) {
|
||||||
{
|
|
||||||
const int cand = (m_next + n) % kSlots;
|
const int cand = (m_next + n) % kSlots;
|
||||||
if (!m_slots[cand].busy.load(std::memory_order_acquire))
|
if (!m_slots[cand].busy.load(std::memory_order_acquire)) {
|
||||||
{
|
|
||||||
idx = cand;
|
idx = cand;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (idx < 0)
|
if (idx < 0) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
m_next = (idx + 1) % kSlots;
|
m_next = (idx + 1) % kSlots;
|
||||||
Slot& s = m_slots[idx];
|
Slot& s = m_slots[idx];
|
||||||
if (!ensure_staging(s, w, h))
|
if (!ensure_staging(s, w, h)) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,17 +263,17 @@ bool VkCapture::present(VkImage image, VkFormat fmt, std::uint32_t w, std::uint3
|
|||||||
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||||
b.image = image;
|
b.image = image;
|
||||||
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||||
m_fns.CmdPipelineBarrier(s.cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
m_fns.CmdPipelineBarrier(s.cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0,
|
||||||
0, 0, nullptr, 0, nullptr, 1, &b);
|
nullptr, 0, nullptr, 1, &b);
|
||||||
};
|
};
|
||||||
image_barrier(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
image_barrier(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_MEMORY_READ_BIT,
|
||||||
VK_ACCESS_MEMORY_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
VK_ACCESS_TRANSFER_READ_BIT);
|
||||||
VkBufferImageCopy region{};
|
VkBufferImageCopy region{};
|
||||||
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||||
region.imageExtent = {w, h, 1};
|
region.imageExtent = {w, h, 1};
|
||||||
m_fns.CmdCopyImageToBuffer(s.cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, s.staging, 1, ®ion);
|
m_fns.CmdCopyImageToBuffer(s.cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, s.staging, 1, ®ion);
|
||||||
image_barrier(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
|
image_barrier(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_ACCESS_TRANSFER_READ_BIT,
|
||||||
VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_MEMORY_READ_BIT);
|
VK_ACCESS_MEMORY_READ_BIT);
|
||||||
m_fns.EndCommandBuffer(s.cmd);
|
m_fns.EndCommandBuffer(s.cmd);
|
||||||
|
|
||||||
std::vector<VkPipelineStageFlags> stages(wait_count, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
|
std::vector<VkPipelineStageFlags> stages(wait_count, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
|
||||||
@@ -328,8 +285,7 @@ bool VkCapture::present(VkImage image, VkFormat fmt, std::uint32_t w, std::uint3
|
|||||||
si.pCommandBuffers = &s.cmd;
|
si.pCommandBuffers = &s.cmd;
|
||||||
si.signalSemaphoreCount = 1;
|
si.signalSemaphoreCount = 1;
|
||||||
si.pSignalSemaphores = &s.present_sem;
|
si.pSignalSemaphores = &s.present_sem;
|
||||||
if (m_fns.QueueSubmit(m_queue, 1, &si, s.fence) != VK_SUCCESS)
|
if (m_fns.QueueSubmit(m_queue, 1, &si, s.fence) != VK_SUCCESS) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
s.w = w;
|
s.w = w;
|
||||||
@@ -348,8 +304,7 @@ bool VkCapture::present(VkImage image, VkFormat fmt, std::uint32_t w, std::uint3
|
|||||||
void VkCapture::reap_slot(Slot& s)
|
void VkCapture::reap_slot(Slot& s)
|
||||||
{
|
{
|
||||||
m_fns.WaitForFences(m_device, 1, &s.fence, VK_TRUE, UINT64_MAX);
|
m_fns.WaitForFences(m_device, 1, &s.fence, VK_TRUE, UINT64_MAX);
|
||||||
if (!s.coherent)
|
if (!s.coherent) {
|
||||||
{
|
|
||||||
VkMappedMemoryRange r{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
|
VkMappedMemoryRange r{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
|
||||||
r.memory = s.mem;
|
r.memory = s.mem;
|
||||||
r.offset = 0;
|
r.offset = 0;
|
||||||
@@ -359,43 +314,35 @@ void VkCapture::reap_slot(Slot& s)
|
|||||||
|
|
||||||
const bool bgra = s.fmt == VK_FORMAT_B8G8R8A8_UNORM || s.fmt == VK_FORMAT_B8G8R8A8_SRGB;
|
const bool bgra = s.fmt == VK_FORMAT_B8G8R8A8_UNORM || s.fmt == VK_FORMAT_B8G8R8A8_SRGB;
|
||||||
const size_t row = static_cast<size_t>(s.w) * 4;
|
const size_t row = static_cast<size_t>(s.w) * 4;
|
||||||
if (m_rgba.size() != row * s.h)
|
if (m_rgba.size() != row * s.h) {
|
||||||
{
|
|
||||||
m_rgba.resize(row * s.h);
|
m_rgba.resize(row * s.h);
|
||||||
}
|
}
|
||||||
const auto* src = static_cast<const unsigned char*>(s.mapped);
|
const auto* src = static_cast<const unsigned char*>(s.mapped);
|
||||||
for (std::uint32_t y = 0; y < s.h; ++y)
|
for (std::uint32_t y = 0; y < s.h; ++y) {
|
||||||
{
|
|
||||||
const unsigned char* in = src + static_cast<size_t>(y) * row;
|
const unsigned char* in = src + static_cast<size_t>(y) * row;
|
||||||
unsigned char* o = m_rgba.data() + static_cast<size_t>(y) * row;
|
unsigned char* o = m_rgba.data() + static_cast<size_t>(y) * row;
|
||||||
if (bgra)
|
if (bgra) {
|
||||||
{
|
for (std::uint32_t x = 0; x < s.w; ++x) {
|
||||||
for (std::uint32_t x = 0; x < s.w; ++x)
|
|
||||||
{
|
|
||||||
o[x * 4 + 0] = in[x * 4 + 2];
|
o[x * 4 + 0] = in[x * 4 + 2];
|
||||||
o[x * 4 + 1] = in[x * 4 + 1];
|
o[x * 4 + 1] = in[x * 4 + 1];
|
||||||
o[x * 4 + 2] = in[x * 4 + 0];
|
o[x * 4 + 2] = in[x * 4 + 0];
|
||||||
o[x * 4 + 3] = 255;
|
o[x * 4 + 3] = 255;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
std::memcpy(o, in, row);
|
std::memcpy(o, in, row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool published = false;
|
bool published = false;
|
||||||
if (ensure_d3d() && m_shared.ensure(m_d3d, s.w, s.h, DXGI_FORMAT_R8G8B8A8_UNORM, m_pid, "vk") &&
|
if (ensure_d3d() && m_shared.ensure(m_d3d, s.w, s.h, DXGI_FORMAT_R8G8B8A8_UNORM, m_pid, "vk")
|
||||||
m_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK)
|
&& m_shared.mutex()->AcquireSync(kVideoMutexKey, 8) == S_OK) {
|
||||||
{
|
|
||||||
m_d3d_ctx->UpdateSubresource(m_shared.texture(), 0, nullptr, m_rgba.data(), static_cast<UINT>(row), 0);
|
m_d3d_ctx->UpdateSubresource(m_shared.texture(), 0, nullptr, m_rgba.data(), static_cast<UINT>(row), 0);
|
||||||
m_d3d_ctx->Flush();
|
m_d3d_ctx->Flush();
|
||||||
m_shared.mutex()->ReleaseSync(kVideoMutexKey);
|
m_shared.mutex()->ReleaseSync(kVideoMutexKey);
|
||||||
published = true;
|
published = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (published)
|
if (published) {
|
||||||
{
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lk(m_last_mutex);
|
std::lock_guard<std::mutex> lk(m_last_mutex);
|
||||||
m_last = m_rgba;
|
m_last = m_rgba;
|
||||||
@@ -403,8 +350,7 @@ void VkCapture::reap_slot(Slot& s)
|
|||||||
m_last_h = s.h;
|
m_last_h = s.h;
|
||||||
}
|
}
|
||||||
m_published.fetch_add(1, std::memory_order_relaxed);
|
m_published.fetch_add(1, std::memory_order_relaxed);
|
||||||
if (m_on_frame)
|
if (m_on_frame) {
|
||||||
{
|
|
||||||
m_on_frame(s.w, s.h);
|
m_on_frame(s.w, s.h);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -415,14 +361,12 @@ void VkCapture::reap_slot(Slot& s)
|
|||||||
|
|
||||||
void VkCapture::reaper_main()
|
void VkCapture::reaper_main()
|
||||||
{
|
{
|
||||||
for (;;)
|
for (;;) {
|
||||||
{
|
|
||||||
int idx;
|
int idx;
|
||||||
{
|
{
|
||||||
std::unique_lock<std::mutex> lk(m_q_mutex);
|
std::unique_lock<std::mutex> lk(m_q_mutex);
|
||||||
m_q_cv.wait(lk, [this] { return m_stop || !m_pending.empty(); });
|
m_q_cv.wait(lk, [this] { return m_stop || !m_pending.empty(); });
|
||||||
if (m_stop && m_pending.empty())
|
if (m_stop && m_pending.empty()) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
idx = m_pending.front();
|
idx = m_pending.front();
|
||||||
@@ -434,8 +378,7 @@ void VkCapture::reaper_main()
|
|||||||
|
|
||||||
void VkCapture::shutdown()
|
void VkCapture::shutdown()
|
||||||
{
|
{
|
||||||
if (m_reaper.joinable())
|
if (m_reaper.joinable()) {
|
||||||
{
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lk(m_q_mutex);
|
std::lock_guard<std::mutex> lk(m_q_mutex);
|
||||||
m_stop = true;
|
m_stop = true;
|
||||||
@@ -445,10 +388,8 @@ void VkCapture::shutdown()
|
|||||||
}
|
}
|
||||||
// The reaper is gone (no more submits/reads); drain any GPU work still referencing our
|
// The reaper is gone (no more submits/reads); drain any GPU work still referencing our
|
||||||
// resources, then free.
|
// resources, then free.
|
||||||
if (m_device != VK_NULL_HANDLE)
|
if (m_device != VK_NULL_HANDLE) {
|
||||||
{
|
if (m_fns.DeviceWaitIdle != nullptr) {
|
||||||
if (m_fns.DeviceWaitIdle != nullptr)
|
|
||||||
{
|
|
||||||
m_fns.DeviceWaitIdle(m_device);
|
m_fns.DeviceWaitIdle(m_device);
|
||||||
}
|
}
|
||||||
free_slots();
|
free_slots();
|
||||||
@@ -463,8 +404,7 @@ void VkCapture::shutdown()
|
|||||||
bool VkCapture::last_frame(std::vector<unsigned char>& out, std::uint32_t& w, std::uint32_t& h)
|
bool VkCapture::last_frame(std::vector<unsigned char>& out, std::uint32_t& w, std::uint32_t& h)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lk(m_last_mutex);
|
std::lock_guard<std::mutex> lk(m_last_mutex);
|
||||||
if (m_last.empty())
|
if (m_last.empty()) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
out = m_last;
|
out = m_last;
|
||||||
|
|||||||
@@ -36,16 +36,13 @@
|
|||||||
|
|
||||||
#include "shared_video_texture.hpp"
|
#include "shared_video_texture.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
class VkCapture
|
class VkCapture {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
// Device entry points the read-back needs (resolved by the caller via the real
|
// Device entry points the read-back needs (resolved by the caller via the real
|
||||||
// vkGetDeviceProcAddr; GetPhysicalDeviceMemoryProperties is instance-level).
|
// vkGetDeviceProcAddr; GetPhysicalDeviceMemoryProperties is instance-level).
|
||||||
struct Fns
|
struct Fns {
|
||||||
{
|
|
||||||
PFN_vkGetDeviceQueue GetDeviceQueue;
|
PFN_vkGetDeviceQueue GetDeviceQueue;
|
||||||
PFN_vkCreateCommandPool CreateCommandPool;
|
PFN_vkCreateCommandPool CreateCommandPool;
|
||||||
PFN_vkDestroyCommandPool DestroyCommandPool;
|
PFN_vkDestroyCommandPool DestroyCommandPool;
|
||||||
@@ -84,8 +81,8 @@ public:
|
|||||||
// Bind to the game's device + queue family and start the reaper thread. `pid` names the shared
|
// Bind to the game's device + queue family and start the reaper thread. `pid` names the shared
|
||||||
// texture (video_share_name). `on_frame(w,h)` runs on the reaper thread after each frame is
|
// texture (video_share_name). `on_frame(w,h)` runs on the reaper thread after each frame is
|
||||||
// published (the caller does its own IPC / stat bookkeeping there). Idempotent-ish: call once.
|
// published (the caller does its own IPC / stat bookkeeping there). Idempotent-ish: call once.
|
||||||
void init(VkPhysicalDevice phys, VkDevice device, std::uint32_t queue_family, const Fns& fns,
|
void init(VkPhysicalDevice phys, VkDevice device, std::uint32_t queue_family, const Fns& fns, unsigned long pid,
|
||||||
unsigned long pid, std::function<void(std::uint32_t, std::uint32_t)> on_frame);
|
std::function<void(std::uint32_t, std::uint32_t)> on_frame);
|
||||||
|
|
||||||
bool active() const { return m_device != VK_NULL_HANDLE; }
|
bool active() const { return m_device != VK_NULL_HANDLE; }
|
||||||
|
|
||||||
@@ -110,8 +107,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
static constexpr int kSlots = 4; // in-flight copies; also the present-semaphore reuse slack
|
static constexpr int kSlots = 4; // in-flight copies; also the present-semaphore reuse slack
|
||||||
|
|
||||||
struct Slot
|
struct Slot {
|
||||||
{
|
|
||||||
VkCommandBuffer cmd = VK_NULL_HANDLE;
|
VkCommandBuffer cmd = VK_NULL_HANDLE;
|
||||||
VkFence fence = VK_NULL_HANDLE;
|
VkFence fence = VK_NULL_HANDLE;
|
||||||
VkSemaphore present_sem = VK_NULL_HANDLE;
|
VkSemaphore present_sem = VK_NULL_HANDLE;
|
||||||
|
|||||||
@@ -24,11 +24,9 @@
|
|||||||
#include "hook_registry.hpp"
|
#include "hook_registry.hpp"
|
||||||
#include "vk_capture.hpp"
|
#include "vk_capture.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
DetourGate g_gate; // drains in-flight present/create detours before remove frees the Vulkan state
|
DetourGate g_gate; // drains in-flight present/create detours before remove frees the Vulkan state
|
||||||
// Capture gate. Unlike the other backends, the game caches our hk_vkQueuePresentKHR pointer at
|
// Capture gate. Unlike the other backends, the game caches our hk_vkQueuePresentKHR pointer at
|
||||||
@@ -65,8 +63,7 @@ std::uint32_t g_qfam = 0;
|
|||||||
VkCapture g_cap; // the shared, off-present-thread read-back (same component the layer uses)
|
VkCapture g_cap; // the shared, off-present-thread read-back (same component the layer uses)
|
||||||
|
|
||||||
// Tracked swap chains (small; engines have one or two).
|
// Tracked swap chains (small; engines have one or two).
|
||||||
struct SwapInfo
|
struct SwapInfo {
|
||||||
{
|
|
||||||
VkSwapchainKHR sc;
|
VkSwapchainKHR sc;
|
||||||
VkFormat fmt;
|
VkFormat fmt;
|
||||||
std::uint32_t w;
|
std::uint32_t w;
|
||||||
@@ -95,10 +92,8 @@ PFN_vkVoidFunction real_gipa(VkInstance inst, const char* name)
|
|||||||
// (copy out what you need before unlocking, since another thread can push_back and reallocate).
|
// (copy out what you need before unlocking, since another thread can push_back and reallocate).
|
||||||
const SwapInfo* find_swap(VkSwapchainKHR sc)
|
const SwapInfo* find_swap(VkSwapchainKHR sc)
|
||||||
{
|
{
|
||||||
for (const SwapInfo& s : g_swaps)
|
for (const SwapInfo& s : g_swaps) {
|
||||||
{
|
if (s.sc == sc) {
|
||||||
if (s.sc == sc)
|
|
||||||
{
|
|
||||||
return &s;
|
return &s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,17 +105,15 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkQueuePresentKHR(VkQueue queue, const VkPrese
|
|||||||
DetourGate::Guard guard(g_gate); // keep the read-back resources alive for this whole detour
|
DetourGate::Guard guard(g_gate); // keep the read-back resources alive for this whole detour
|
||||||
hook_note_call(g_id_present);
|
hook_note_call(g_id_present);
|
||||||
g_presents.fetch_add(1, std::memory_order_relaxed);
|
g_presents.fetch_add(1, std::memory_order_relaxed);
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->note_present();
|
g_ipc->note_present();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture only the simple, common single-swapchain present; pass anything else through. The
|
// Capture only the simple, common single-swapchain present; pass anything else through. The
|
||||||
// gate lets removal stop capture (and pass through to the real present) before it frees the
|
// gate lets removal stop capture (and pass through to the real present) before it frees the
|
||||||
// read-back state, even though the game keeps calling this cached detour pointer.
|
// read-back state, even though the game keeps calling this cached detour pointer.
|
||||||
if (g_capture_enabled.load(std::memory_order_acquire) && g_device != VK_NULL_HANDLE &&
|
if (g_capture_enabled.load(std::memory_order_acquire) && g_device != VK_NULL_HANDLE && pPresentInfo != nullptr
|
||||||
pPresentInfo != nullptr && pPresentInfo->swapchainCount == 1)
|
&& pPresentInfo->swapchainCount == 1) {
|
||||||
{
|
|
||||||
// Copy the matched swapchain's fields out under the lock, then capture without holding it (so
|
// Copy the matched swapchain's fields out under the lock, then capture without holding it (so
|
||||||
// the GPU submit can't block a concurrent create, and the SwapInfo* can't dangle on a realloc).
|
// the GPU submit can't block a concurrent create, and the SwapInfo* can't dangle on a realloc).
|
||||||
VkImage image = VK_NULL_HANDLE;
|
VkImage image = VK_NULL_HANDLE;
|
||||||
@@ -131,8 +124,7 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkQueuePresentKHR(VkQueue queue, const VkPrese
|
|||||||
std::scoped_lock lock(g_swaps_mutex);
|
std::scoped_lock lock(g_swaps_mutex);
|
||||||
const SwapInfo* s = find_swap(pPresentInfo->pSwapchains[0]);
|
const SwapInfo* s = find_swap(pPresentInfo->pSwapchains[0]);
|
||||||
const std::uint32_t idx = pPresentInfo->pImageIndices[0];
|
const std::uint32_t idx = pPresentInfo->pImageIndices[0];
|
||||||
if (s != nullptr && idx < s->images.size())
|
if (s != nullptr && idx < s->images.size()) {
|
||||||
{
|
|
||||||
image = s->images[idx];
|
image = s->images[idx];
|
||||||
fmt = s->fmt;
|
fmt = s->fmt;
|
||||||
w = s->w;
|
w = s->w;
|
||||||
@@ -140,12 +132,10 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkQueuePresentKHR(VkQueue queue, const VkPrese
|
|||||||
matched = true;
|
matched = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (matched)
|
if (matched) {
|
||||||
{
|
|
||||||
VkSemaphore chained = VK_NULL_HANDLE;
|
VkSemaphore chained = VK_NULL_HANDLE;
|
||||||
if (g_cap.present(image, fmt, w, h, pPresentInfo->pWaitSemaphores,
|
if (g_cap.present(image, fmt, w, h, pPresentInfo->pWaitSemaphores, pPresentInfo->waitSemaphoreCount,
|
||||||
pPresentInfo->waitSemaphoreCount, chained))
|
chained)) {
|
||||||
{
|
|
||||||
// Replace the present's wait with our chained semaphore (our submit consumed the
|
// Replace the present's wait with our chained semaphore (our submit consumed the
|
||||||
// originals and signals this one), so the present still orders after rendering.
|
// originals and signals this one), so the present still orders after rendering.
|
||||||
VkPresentInfoKHR pi = *pPresentInfo;
|
VkPresentInfoKHR pi = *pPresentInfo;
|
||||||
@@ -163,8 +153,7 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateSwapchainKHR(VkDevice device, const Vk
|
|||||||
{
|
{
|
||||||
DetourGate::Guard guard(g_gate); // keep g_swaps stable while remove may be clearing it
|
DetourGate::Guard guard(g_gate); // keep g_swaps stable while remove may be clearing it
|
||||||
const VkResult r = g_real_create_swapchain(device, ci, alloc, out);
|
const VkResult r = g_real_create_swapchain(device, ci, alloc, out);
|
||||||
if (r == VK_SUCCESS && out != nullptr && g_get_swapchain_images != nullptr)
|
if (r == VK_SUCCESS && out != nullptr && g_get_swapchain_images != nullptr) {
|
||||||
{
|
|
||||||
SwapInfo info{};
|
SwapInfo info{};
|
||||||
info.sc = *out;
|
info.sc = *out;
|
||||||
info.fmt = ci->imageFormat;
|
info.fmt = ci->imageFormat;
|
||||||
@@ -178,12 +167,11 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateSwapchainKHR(VkDevice device, const Vk
|
|||||||
std::scoped_lock lock(g_swaps_mutex);
|
std::scoped_lock lock(g_swaps_mutex);
|
||||||
// De-dup a recycled handle value, then bound growth (drop the oldest; the just-created
|
// De-dup a recycled handle value, then bound growth (drop the oldest; the just-created
|
||||||
// active swapchain is newest and stays).
|
// active swapchain is newest and stays).
|
||||||
g_swaps.erase(std::remove_if(g_swaps.begin(), g_swaps.end(),
|
g_swaps.erase(
|
||||||
[&](const SwapInfo& e) { return e.sc == info.sc; }),
|
std::remove_if(g_swaps.begin(), g_swaps.end(), [&](const SwapInfo& e) { return e.sc == info.sc; }),
|
||||||
g_swaps.end());
|
g_swaps.end());
|
||||||
g_swaps.push_back(std::move(info));
|
g_swaps.push_back(std::move(info));
|
||||||
if (g_swaps.size() > kMaxTrackedSwaps)
|
if (g_swaps.size() > kMaxTrackedSwaps) {
|
||||||
{
|
|
||||||
g_swaps.erase(g_swaps.begin());
|
g_swaps.erase(g_swaps.begin());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,8 +224,7 @@ void start_capture(VkDevice device)
|
|||||||
// Reaper thread, after each frame is mirrored into the shared texture.
|
// Reaper thread, after each frame is mirrored into the shared texture.
|
||||||
g_present_captured.store(true, std::memory_order_relaxed);
|
g_present_captured.store(true, std::memory_order_relaxed);
|
||||||
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
|
g_frames_shared.fetch_add(1, std::memory_order_relaxed);
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->publish_video_frame(w, h, static_cast<std::uint32_t>(DXGI_FORMAT_R8G8B8A8_UNORM));
|
g_ipc->publish_video_frame(w, h, static_cast<std::uint32_t>(DXGI_FORMAT_R8G8B8A8_UNORM));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -254,8 +241,7 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateDevice(VkPhysicalDevice phys, const Vk
|
|||||||
g_device = *out;
|
g_device = *out;
|
||||||
g_qfam = ci->queueCreateInfoCount > 0 ? ci->pQueueCreateInfos[0].queueFamilyIndex : 0;
|
g_qfam = ci->queueCreateInfoCount > 0 ? ci->pQueueCreateInfos[0].queueFamilyIndex : 0;
|
||||||
g_real_gdpa = reinterpret_cast<PFN_vkGetDeviceProcAddr>(real_gipa(g_instance, "vkGetDeviceProcAddr"));
|
g_real_gdpa = reinterpret_cast<PFN_vkGetDeviceProcAddr>(real_gipa(g_instance, "vkGetDeviceProcAddr"));
|
||||||
g_real_create_swapchain =
|
g_real_create_swapchain = reinterpret_cast<PFN_vkCreateSwapchainKHR>(g_real_gdpa(*out, "vkCreateSwapchainKHR"));
|
||||||
reinterpret_cast<PFN_vkCreateSwapchainKHR>(g_real_gdpa(*out, "vkCreateSwapchainKHR"));
|
|
||||||
g_real_present = reinterpret_cast<PFN_vkQueuePresentKHR>(g_real_gdpa(*out, "vkQueuePresentKHR"));
|
g_real_present = reinterpret_cast<PFN_vkQueuePresentKHR>(g_real_gdpa(*out, "vkQueuePresentKHR"));
|
||||||
start_capture(*out);
|
start_capture(*out);
|
||||||
// Arm capture only once every real_* pointer + VkCapture is populated (release pairs with the
|
// Arm capture only once every real_* pointer + VkCapture is populated (release pairs with the
|
||||||
@@ -266,13 +252,12 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateDevice(VkPhysicalDevice phys, const Vk
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateInstance(const VkInstanceCreateInfo* ci,
|
VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateInstance(const VkInstanceCreateInfo* ci, const VkAllocationCallbacks* alloc,
|
||||||
const VkAllocationCallbacks* alloc, VkInstance* out)
|
VkInstance* out)
|
||||||
{
|
{
|
||||||
auto real_create = reinterpret_cast<PFN_vkCreateInstance>(real_gipa(nullptr, "vkCreateInstance"));
|
auto real_create = reinterpret_cast<PFN_vkCreateInstance>(real_gipa(nullptr, "vkCreateInstance"));
|
||||||
const VkResult r = real_create(ci, alloc, out);
|
const VkResult r = real_create(ci, alloc, out);
|
||||||
if (r == VK_SUCCESS && out != nullptr)
|
if (r == VK_SUCCESS && out != nullptr) {
|
||||||
{
|
|
||||||
g_instance = *out;
|
g_instance = *out;
|
||||||
g_real_create_device = reinterpret_cast<PFN_vkCreateDevice>(real_gipa(*out, "vkCreateDevice"));
|
g_real_create_device = reinterpret_cast<PFN_vkCreateDevice>(real_gipa(*out, "vkCreateDevice"));
|
||||||
logf("vk: instance created -- intercepting device/swapchain/present");
|
logf("vk: instance created -- intercepting device/swapchain/present");
|
||||||
@@ -282,14 +267,11 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateInstance(const VkInstanceCreateInfo* c
|
|||||||
|
|
||||||
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hk_vkGetDeviceProcAddr(VkDevice device, const char* name)
|
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hk_vkGetDeviceProcAddr(VkDevice device, const char* name)
|
||||||
{
|
{
|
||||||
if (name != nullptr)
|
if (name != nullptr) {
|
||||||
{
|
if (std::strcmp(name, "vkQueuePresentKHR") == 0) {
|
||||||
if (std::strcmp(name, "vkQueuePresentKHR") == 0)
|
|
||||||
{
|
|
||||||
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkQueuePresentKHR);
|
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkQueuePresentKHR);
|
||||||
}
|
}
|
||||||
if (std::strcmp(name, "vkCreateSwapchainKHR") == 0)
|
if (std::strcmp(name, "vkCreateSwapchainKHR") == 0) {
|
||||||
{
|
|
||||||
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkCreateSwapchainKHR);
|
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkCreateSwapchainKHR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,22 +280,17 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hk_vkGetDeviceProcAddr(VkDevice device,
|
|||||||
|
|
||||||
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hk_vkGetInstanceProcAddr(VkInstance instance, const char* name)
|
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hk_vkGetInstanceProcAddr(VkInstance instance, const char* name)
|
||||||
{
|
{
|
||||||
if (name != nullptr)
|
if (name != nullptr) {
|
||||||
{
|
if (std::strcmp(name, "vkGetInstanceProcAddr") == 0) {
|
||||||
if (std::strcmp(name, "vkGetInstanceProcAddr") == 0)
|
|
||||||
{
|
|
||||||
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkGetInstanceProcAddr);
|
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkGetInstanceProcAddr);
|
||||||
}
|
}
|
||||||
if (std::strcmp(name, "vkCreateInstance") == 0)
|
if (std::strcmp(name, "vkCreateInstance") == 0) {
|
||||||
{
|
|
||||||
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkCreateInstance);
|
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkCreateInstance);
|
||||||
}
|
}
|
||||||
if (std::strcmp(name, "vkCreateDevice") == 0)
|
if (std::strcmp(name, "vkCreateDevice") == 0) {
|
||||||
{
|
|
||||||
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkCreateDevice);
|
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkCreateDevice);
|
||||||
}
|
}
|
||||||
if (std::strcmp(name, "vkGetDeviceProcAddr") == 0)
|
if (std::strcmp(name, "vkGetDeviceProcAddr") == 0) {
|
||||||
{
|
|
||||||
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkGetDeviceProcAddr);
|
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkGetDeviceProcAddr);
|
||||||
}
|
}
|
||||||
// vkGetInstanceProcAddr can also resolve device-level functions (the loader returns a
|
// vkGetInstanceProcAddr can also resolve device-level functions (the loader returns a
|
||||||
@@ -321,12 +298,10 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hk_vkGetInstanceProcAddr(VkInstance ins
|
|||||||
// swapchain entry points this way (rather than via vkGetDeviceProcAddr) would otherwise get
|
// swapchain entry points this way (rather than via vkGetDeviceProcAddr) would otherwise get
|
||||||
// the real loader pointer and bypass our capture, so intercept them here too. (Our detours
|
// the real loader pointer and bypass our capture, so intercept them here too. (Our detours
|
||||||
// gate on g_capture_enabled / g_device, so handing them out before the device exists is safe.)
|
// gate on g_capture_enabled / g_device, so handing them out before the device exists is safe.)
|
||||||
if (std::strcmp(name, "vkQueuePresentKHR") == 0)
|
if (std::strcmp(name, "vkQueuePresentKHR") == 0) {
|
||||||
{
|
|
||||||
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkQueuePresentKHR);
|
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkQueuePresentKHR);
|
||||||
}
|
}
|
||||||
if (std::strcmp(name, "vkCreateSwapchainKHR") == 0)
|
if (std::strcmp(name, "vkCreateSwapchainKHR") == 0) {
|
||||||
{
|
|
||||||
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkCreateSwapchainKHR);
|
return reinterpret_cast<PFN_vkVoidFunction>(&hk_vkCreateSwapchainKHR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,8 +311,7 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL hk_vkGetInstanceProcAddr(VkInstance ins
|
|||||||
void* gipa_export_address()
|
void* gipa_export_address()
|
||||||
{
|
{
|
||||||
HMODULE vk = GetModuleHandleW(L"vulkan-1.dll");
|
HMODULE vk = GetModuleHandleW(L"vulkan-1.dll");
|
||||||
if (vk == nullptr)
|
if (vk == nullptr) {
|
||||||
{
|
|
||||||
return nullptr; // not a Vulkan process (yet)
|
return nullptr; // not a Vulkan process (yet)
|
||||||
}
|
}
|
||||||
return reinterpret_cast<void*>(GetProcAddress(vk, "vkGetInstanceProcAddr"));
|
return reinterpret_cast<void*>(GetProcAddress(vk, "vkGetInstanceProcAddr"));
|
||||||
@@ -349,17 +323,14 @@ bool install_vk_hooks(IpcClient& ipc)
|
|||||||
{
|
{
|
||||||
g_ipc = &ipc;
|
g_ipc = &ipc;
|
||||||
g_pid = GetCurrentProcessId();
|
g_pid = GetCurrentProcessId();
|
||||||
if (g_hk_gipa.enabled())
|
if (g_hk_gipa.enabled()) {
|
||||||
{
|
|
||||||
return true; // already installed (persistent hook; re-install below re-enables it)
|
return true; // already installed (persistent hook; re-install below re-enables it)
|
||||||
}
|
}
|
||||||
if (g_id_present < 0)
|
if (g_id_present < 0) {
|
||||||
{
|
|
||||||
g_id_present = hook_register("vkQueuePresentKHR", HookSubsys_Video);
|
g_id_present = hook_register("vkQueuePresentKHR", HookSubsys_Video);
|
||||||
}
|
}
|
||||||
void* gipa = gipa_export_address();
|
void* gipa = gipa_export_address();
|
||||||
if (gipa == nullptr)
|
if (gipa == nullptr) {
|
||||||
{
|
|
||||||
hook_set_installed(g_id_present, false);
|
hook_set_installed(g_id_present, false);
|
||||||
return false; // vulkan-1.dll not loaded; caller can retry once the game loads it
|
return false; // vulkan-1.dll not loaded; caller can retry once the game loads it
|
||||||
}
|
}
|
||||||
@@ -420,8 +391,7 @@ bool vk_injected_too_late()
|
|||||||
// hook (we're not in the chain). A game we hooked early always trips hk_vkCreateDevice
|
// hook (we're not in the chain). A game we hooked early always trips hk_vkCreateDevice
|
||||||
// (g_device != null) well within the grace window, even before it presents. The host shows
|
// (g_device != null) well within the grace window, even before it presents. The host shows
|
||||||
// the "relaunch with Auto-attach / Vulkan layer" banner on this.
|
// the "relaunch with Auto-attach / Vulkan layer" banner on this.
|
||||||
if (GetModuleHandleW(L"vulkan-1.dll") == nullptr || g_device != VK_NULL_HANDLE || g_install_tick == 0)
|
if (GetModuleHandleW(L"vulkan-1.dll") == nullptr || g_device != VK_NULL_HANDLE || g_install_tick == 0) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return (GetTickCount64() - g_install_tick) > 4000;
|
return (GetTickCount64() - g_install_tick) > 4000;
|
||||||
|
|||||||
@@ -10,8 +10,7 @@
|
|||||||
|
|
||||||
#include "ipc_client.hpp"
|
#include "ipc_client.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
// Installs the Vulkan capture hook (inline-hooks vkGetInstanceProcAddr). `ipc` must outlive the
|
// Installs the Vulkan capture hook (inline-hooks vkGetInstanceProcAddr). `ipc` must outlive the
|
||||||
// hook. Returns true if vulkan-1.dll is loaded and the export was hooked; false otherwise, so
|
// hook. Returns true if vulkan-1.dll is loaded and the export was hooked; false otherwise, so
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
// Read a COM object's vtable slot (e.g. to grab a method's address off a probe object for an
|
// Read a COM object's vtable slot (e.g. to grab a method's address off a probe object for an
|
||||||
// inline hook).
|
// inline hook).
|
||||||
@@ -21,19 +20,16 @@ inline void* vtable_method(void* obj, unsigned index)
|
|||||||
return (*reinterpret_cast<void***>(obj))[index];
|
return (*reinterpret_cast<void***>(obj))[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
class VtableHook
|
class VtableHook {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
bool install(void* com_object, unsigned index, void* detour)
|
bool install(void* com_object, unsigned index, void* detour)
|
||||||
{
|
{
|
||||||
if (m_vtable != nullptr)
|
if (m_vtable != nullptr) {
|
||||||
{
|
|
||||||
return true; // already installed (shared vtable covers every instance)
|
return true; // already installed (shared vtable covers every instance)
|
||||||
}
|
}
|
||||||
auto** vtable = *reinterpret_cast<void***>(com_object);
|
auto** vtable = *reinterpret_cast<void***>(com_object);
|
||||||
DWORD old_protect = 0;
|
DWORD old_protect = 0;
|
||||||
if (!VirtualProtect(&vtable[index], sizeof(void*), PAGE_READWRITE, &old_protect))
|
if (!VirtualProtect(&vtable[index], sizeof(void*), PAGE_READWRITE, &old_protect)) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
m_original = vtable[index];
|
m_original = vtable[index];
|
||||||
@@ -46,13 +42,11 @@ public:
|
|||||||
|
|
||||||
void remove()
|
void remove()
|
||||||
{
|
{
|
||||||
if (m_vtable == nullptr)
|
if (m_vtable == nullptr) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DWORD old_protect = 0;
|
DWORD old_protect = 0;
|
||||||
if (VirtualProtect(&m_vtable[m_index], sizeof(void*), PAGE_READWRITE, &old_protect))
|
if (VirtualProtect(&m_vtable[m_index], sizeof(void*), PAGE_READWRITE, &old_protect)) {
|
||||||
{
|
|
||||||
m_vtable[m_index] = m_original;
|
m_vtable[m_index] = m_original;
|
||||||
VirtualProtect(&m_vtable[m_index], sizeof(void*), old_protect, &old_protect);
|
VirtualProtect(&m_vtable[m_index], sizeof(void*), old_protect, &old_protect);
|
||||||
}
|
}
|
||||||
@@ -63,7 +57,11 @@ public:
|
|||||||
m_index = 0;
|
m_index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename Fn> Fn original() const { return reinterpret_cast<Fn>(m_original); }
|
template <typename Fn>
|
||||||
|
Fn original() const
|
||||||
|
{
|
||||||
|
return reinterpret_cast<Fn>(m_original);
|
||||||
|
}
|
||||||
explicit operator bool() const { return m_vtable != nullptr; }
|
explicit operator bool() const { return m_vtable != nullptr; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -13,11 +13,9 @@
|
|||||||
#include "hook_install.hpp"
|
#include "hook_install.hpp"
|
||||||
#include "hook_registry.hpp"
|
#include "hook_registry.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
DetourGate g_gate; // drains in-flight XInput detours before remove nulls the IPC pointer
|
DetourGate g_gate; // drains in-flight XInput detours before remove nulls the IPC pointer
|
||||||
|
|
||||||
@@ -41,16 +39,13 @@ std::array<CoopPadState, kMaxPads> g_cache;
|
|||||||
|
|
||||||
void refresh_cache()
|
void refresh_cache()
|
||||||
{
|
{
|
||||||
if (g_ipc == nullptr)
|
if (g_ipc == nullptr) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
CoopPadState pads[kMaxPads];
|
CoopPadState pads[kMaxPads];
|
||||||
std::uint32_t count = 0;
|
std::uint32_t count = 0;
|
||||||
if (g_ipc->snapshot(pads, count))
|
if (g_ipc->snapshot(pads, count)) {
|
||||||
{
|
for (std::uint32_t i = 0; i < kMaxPads; ++i) {
|
||||||
for (std::uint32_t i = 0; i < kMaxPads; ++i)
|
|
||||||
{
|
|
||||||
g_cache[i] = pads[i];
|
g_cache[i] = pads[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,31 +66,26 @@ void fill_gamepad(const CoopPadState& pad, XINPUT_GAMEPAD& out)
|
|||||||
// (documented) XInputGetState, which must not report it.
|
// (documented) XInputGetState, which must not report it.
|
||||||
DWORD query_state(DWORD user_index, XINPUT_STATE* state, bool keep_guide)
|
DWORD query_state(DWORD user_index, XINPUT_STATE* state, bool keep_guide)
|
||||||
{
|
{
|
||||||
if (state == nullptr || user_index >= kMaxPads)
|
if (state == nullptr || user_index >= kMaxPads) {
|
||||||
{
|
|
||||||
return ERROR_DEVICE_NOT_CONNECTED;
|
return ERROR_DEVICE_NOT_CONNECTED;
|
||||||
}
|
}
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->note_state_query(user_index); // proves to the host the game is polling us
|
g_ipc->note_state_query(user_index); // proves to the host the game is polling us
|
||||||
}
|
}
|
||||||
refresh_cache();
|
refresh_cache();
|
||||||
const CoopPadState& pad = g_cache[user_index];
|
const CoopPadState& pad = g_cache[user_index];
|
||||||
if (!pad.connected)
|
if (!pad.connected) {
|
||||||
{
|
|
||||||
return ERROR_DEVICE_NOT_CONNECTED;
|
return ERROR_DEVICE_NOT_CONNECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
XINPUT_STATE result = {};
|
XINPUT_STATE result = {};
|
||||||
result.dwPacketNumber = pad.packet;
|
result.dwPacketNumber = pad.packet;
|
||||||
fill_gamepad(pad, result.Gamepad);
|
fill_gamepad(pad, result.Gamepad);
|
||||||
if (!keep_guide)
|
if (!keep_guide) {
|
||||||
{
|
|
||||||
result.Gamepad.wButtons &= ~kGuideButton;
|
result.Gamepad.wButtons &= ~kGuideButton;
|
||||||
}
|
}
|
||||||
*state = result;
|
*state = result;
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->note_read_state(user_index, pad); // round-trip: what the game just read
|
g_ipc->note_read_state(user_index, pad); // round-trip: what the game just read
|
||||||
}
|
}
|
||||||
return ERROR_SUCCESS;
|
return ERROR_SUCCESS;
|
||||||
@@ -119,17 +109,14 @@ DWORD WINAPI hk_XInputGetCapabilities(DWORD user_index, DWORD /*flags*/, XINPUT_
|
|||||||
{
|
{
|
||||||
DetourGate::Guard guard(g_gate); // keep g_ipc valid for this whole detour
|
DetourGate::Guard guard(g_gate); // keep g_ipc valid for this whole detour
|
||||||
hook_note_call(g_id_getcaps);
|
hook_note_call(g_id_getcaps);
|
||||||
if (caps == nullptr || user_index >= kMaxPads)
|
if (caps == nullptr || user_index >= kMaxPads) {
|
||||||
{
|
|
||||||
return ERROR_DEVICE_NOT_CONNECTED;
|
return ERROR_DEVICE_NOT_CONNECTED;
|
||||||
}
|
}
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->note_caps_query(user_index);
|
g_ipc->note_caps_query(user_index);
|
||||||
}
|
}
|
||||||
refresh_cache();
|
refresh_cache();
|
||||||
if (!g_cache[user_index].connected)
|
if (!g_cache[user_index].connected) {
|
||||||
{
|
|
||||||
return ERROR_DEVICE_NOT_CONNECTED;
|
return ERROR_DEVICE_NOT_CONNECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,12 +143,10 @@ DWORD WINAPI hk_XInputSetState(DWORD user_index, XINPUT_VIBRATION* vibration)
|
|||||||
{
|
{
|
||||||
DetourGate::Guard guard(g_gate); // keep g_ipc valid for this whole detour
|
DetourGate::Guard guard(g_gate); // keep g_ipc valid for this whole detour
|
||||||
hook_note_call(g_id_setstate);
|
hook_note_call(g_id_setstate);
|
||||||
if (user_index >= kMaxPads || !g_cache[user_index].connected)
|
if (user_index >= kMaxPads || !g_cache[user_index].connected) {
|
||||||
{
|
|
||||||
return ERROR_DEVICE_NOT_CONNECTED;
|
return ERROR_DEVICE_NOT_CONNECTED;
|
||||||
}
|
}
|
||||||
if (g_ipc != nullptr && vibration != nullptr)
|
if (g_ipc != nullptr && vibration != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->note_rumble(user_index, vibration->wLeftMotorSpeed, vibration->wRightMotorSpeed);
|
g_ipc->note_rumble(user_index, vibration->wLeftMotorSpeed, vibration->wRightMotorSpeed);
|
||||||
}
|
}
|
||||||
return ERROR_SUCCESS;
|
return ERROR_SUCCESS;
|
||||||
@@ -170,12 +155,10 @@ DWORD WINAPI hk_XInputSetState(DWORD user_index, XINPUT_VIBRATION* vibration)
|
|||||||
// `name` is a GetProcAddress LPCSTR: an export name, or MAKEINTRESOURCEA(ordinal).
|
// `name` is a GetProcAddress LPCSTR: an export name, or MAKEINTRESOURCEA(ordinal).
|
||||||
void hook_export(HMODULE module, const char* name, void* detour, int registry_id)
|
void hook_export(HMODULE module, const char* name, void* detour, int registry_id)
|
||||||
{
|
{
|
||||||
if (module == nullptr)
|
if (module == nullptr) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (void* target = reinterpret_cast<void*>(GetProcAddress(module, name)))
|
if (void* target = reinterpret_cast<void*>(GetProcAddress(module, name))) {
|
||||||
{
|
|
||||||
g_hooks.emplace_back();
|
g_hooks.emplace_back();
|
||||||
install_inline(g_hooks.back(), target, detour); // assign-then-enable (no install race)
|
install_inline(g_hooks.back(), target, detour); // assign-then-enable (no install race)
|
||||||
hook_set_installed(registry_id, true);
|
hook_set_installed(registry_id, true);
|
||||||
@@ -186,8 +169,7 @@ void hook_export(HMODULE module, const char* name, void* detour, int registry_id
|
|||||||
|
|
||||||
bool install_xinput_hooks(IpcClient& ipc)
|
bool install_xinput_hooks(IpcClient& ipc)
|
||||||
{
|
{
|
||||||
if (!g_hooks.empty())
|
if (!g_hooks.empty()) {
|
||||||
{
|
|
||||||
return true; // already installed
|
return true; // already installed
|
||||||
}
|
}
|
||||||
g_ipc = &ipc;
|
g_ipc = &ipc;
|
||||||
@@ -201,22 +183,18 @@ bool install_xinput_hooks(IpcClient& ipc)
|
|||||||
// A process generally loads exactly one of these, but hook every one that is
|
// A process generally loads exactly one of these, but hook every one that is
|
||||||
// present so we don't miss the one the game actually calls.
|
// present so we don't miss the one the game actually calls.
|
||||||
const wchar_t* modules[] = {L"xinput1_4.dll", L"xinput1_3.dll", L"xinput9_1_0.dll", L"xinputuap.dll"};
|
const wchar_t* modules[] = {L"xinput1_4.dll", L"xinput1_3.dll", L"xinput9_1_0.dll", L"xinputuap.dll"};
|
||||||
for (const wchar_t* name : modules)
|
for (const wchar_t* name : modules) {
|
||||||
{
|
|
||||||
HMODULE module = GetModuleHandleW(name);
|
HMODULE module = GetModuleHandleW(name);
|
||||||
if (module == nullptr)
|
if (module == nullptr) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
hook_export(module, "XInputGetState", reinterpret_cast<void*>(&hk_XInputGetState), g_id_getstate);
|
hook_export(module, "XInputGetState", reinterpret_cast<void*>(&hk_XInputGetState), g_id_getstate);
|
||||||
hook_export(module, MAKEINTRESOURCEA(100), reinterpret_cast<void*>(&hk_XInputGetStateEx),
|
hook_export(module, MAKEINTRESOURCEA(100), reinterpret_cast<void*>(&hk_XInputGetStateEx),
|
||||||
g_id_getstateex); // XInputGetStateEx is exported by ordinal only
|
g_id_getstateex); // XInputGetStateEx is exported by ordinal only
|
||||||
hook_export(module, "XInputGetCapabilities", reinterpret_cast<void*>(&hk_XInputGetCapabilities),
|
hook_export(module, "XInputGetCapabilities", reinterpret_cast<void*>(&hk_XInputGetCapabilities), g_id_getcaps);
|
||||||
g_id_getcaps);
|
|
||||||
hook_export(module, "XInputSetState", reinterpret_cast<void*>(&hk_XInputSetState), g_id_setstate);
|
hook_export(module, "XInputSetState", reinterpret_cast<void*>(&hk_XInputSetState), g_id_setstate);
|
||||||
}
|
}
|
||||||
if (!g_hooks.empty())
|
if (!g_hooks.empty()) {
|
||||||
{
|
|
||||||
g_ipc->mark_attached();
|
g_ipc->mark_attached();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -229,8 +207,7 @@ void remove_xinput_hooks()
|
|||||||
// before nulling the IPC pointer they read. The XInput detours return synthesized pad state and
|
// before nulling the IPC pointer they read. The XInput detours return synthesized pad state and
|
||||||
// never call the trampoline, so (unlike the present/MKB hooks) destroying the vector after the
|
// never call the trampoline, so (unlike the present/MKB hooks) destroying the vector after the
|
||||||
// drain is safe -- there's no live trampoline a stale detour could jump through.
|
// drain is safe -- there's no live trampoline a stale detour could jump through.
|
||||||
for (auto& h : g_hooks)
|
for (auto& h : g_hooks) {
|
||||||
{
|
|
||||||
disable_for_removal(h);
|
disable_for_removal(h);
|
||||||
}
|
}
|
||||||
hook_set_installed(g_id_getstate, false);
|
hook_set_installed(g_id_getstate, false);
|
||||||
@@ -239,8 +216,7 @@ void remove_xinput_hooks()
|
|||||||
hook_set_installed(g_id_setstate, false);
|
hook_set_installed(g_id_setstate, false);
|
||||||
g_gate.drain(); // wait for any in-flight detour before nulling the IPC pointer it reads
|
g_gate.drain(); // wait for any in-flight detour before nulling the IPC pointer it reads
|
||||||
g_hooks.clear(); // no detour in-flight or able to start now -> safe to free the trampolines
|
g_hooks.clear(); // no detour in-flight or able to start now -> safe to free the trampolines
|
||||||
if (g_ipc != nullptr)
|
if (g_ipc != nullptr) {
|
||||||
{
|
|
||||||
g_ipc->mark_detached();
|
g_ipc->mark_detached();
|
||||||
}
|
}
|
||||||
g_ipc = nullptr;
|
g_ipc = nullptr;
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
|
|
||||||
#include "ipc_client.hpp"
|
#include "ipc_client.hpp"
|
||||||
|
|
||||||
namespace coop::hook
|
namespace coop::hook {
|
||||||
{
|
|
||||||
|
|
||||||
// Locates the loaded XInput module(s) and hooks the state/capability entry
|
// Locates the loaded XInput module(s) and hooks the state/capability entry
|
||||||
// points. `ipc` must outlive the hooks. Returns true if at least one module was
|
// points. `ipc` must outlive the hooks. Returns true if at least one module was
|
||||||
|
|||||||
@@ -11,14 +11,11 @@
|
|||||||
#include "audio/process_loopback_capture.hpp"
|
#include "audio/process_loopback_capture.hpp"
|
||||||
#include "coop/audio_correlate.hpp"
|
#include "coop/audio_correlate.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
namespace {
|
||||||
namespace
|
|
||||||
{
|
|
||||||
|
|
||||||
// Resolve a (possibly EXTENSIBLE) WAVEFORMATEX to scalar channels / bits / tag.
|
// Resolve a (possibly EXTENSIBLE) WAVEFORMATEX to scalar channels / bits / tag.
|
||||||
struct ScalarFormat
|
struct ScalarFormat {
|
||||||
{
|
|
||||||
unsigned rate = 0;
|
unsigned rate = 0;
|
||||||
unsigned channels = 0;
|
unsigned channels = 0;
|
||||||
unsigned bits = 0;
|
unsigned bits = 0;
|
||||||
@@ -32,15 +29,11 @@ ScalarFormat resolve(const WAVEFORMATEX* wfx)
|
|||||||
f.channels = wfx->nChannels;
|
f.channels = wfx->nChannels;
|
||||||
f.bits = wfx->wBitsPerSample;
|
f.bits = wfx->wBitsPerSample;
|
||||||
f.tag = wfx->wFormatTag;
|
f.tag = wfx->wFormatTag;
|
||||||
if (wfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE && wfx->cbSize >= 22)
|
if (wfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE && wfx->cbSize >= 22) {
|
||||||
{
|
|
||||||
const auto* ext = reinterpret_cast<const WAVEFORMATEXTENSIBLE*>(wfx);
|
const auto* ext = reinterpret_cast<const WAVEFORMATEXTENSIBLE*>(wfx);
|
||||||
if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)
|
if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {
|
||||||
{
|
|
||||||
f.tag = WAVE_FORMAT_IEEE_FLOAT;
|
f.tag = WAVE_FORMAT_IEEE_FLOAT;
|
||||||
}
|
} else if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_PCM) {
|
||||||
else if (ext->SubFormat == KSDATAFORMAT_SUBTYPE_PCM)
|
|
||||||
{
|
|
||||||
f.tag = WAVE_FORMAT_PCM;
|
f.tag = WAVE_FORMAT_PCM;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,8 +51,7 @@ std::vector<float> to_mono(const std::vector<BYTE>& bytes, const ScalarFormat& f
|
|||||||
|
|
||||||
void drain_ring(AudioRingHeader& ring, std::vector<BYTE>& scratch)
|
void drain_ring(AudioRingHeader& ring, std::vector<BYTE>& scratch)
|
||||||
{
|
{
|
||||||
while (audio_ring_pop(ring, scratch.data(), static_cast<std::uint32_t>(scratch.size())) > 0)
|
while (audio_ring_pop(ring, scratch.data(), static_cast<std::uint32_t>(scratch.size())) > 0) {
|
||||||
{
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,19 +62,16 @@ ChunkedCapture parse_chunks(const std::vector<BYTE>& raw, unsigned stride)
|
|||||||
{
|
{
|
||||||
ChunkedCapture cap;
|
ChunkedCapture cap;
|
||||||
cap.stride = stride;
|
cap.stride = stride;
|
||||||
if (stride == 0)
|
if (stride == 0) {
|
||||||
{
|
|
||||||
return cap;
|
return cap;
|
||||||
}
|
}
|
||||||
std::size_t off = 0;
|
std::size_t off = 0;
|
||||||
while (off + sizeof(std::uint32_t) <= raw.size())
|
while (off + sizeof(std::uint32_t) <= raw.size()) {
|
||||||
{
|
|
||||||
std::uint32_t count = 0;
|
std::uint32_t count = 0;
|
||||||
std::memcpy(&count, raw.data() + off, sizeof(count));
|
std::memcpy(&count, raw.data() + off, sizeof(count));
|
||||||
off += sizeof(count);
|
off += sizeof(count);
|
||||||
const std::size_t payload = static_cast<std::size_t>(count) * stride;
|
const std::size_t payload = static_cast<std::size_t>(count) * stride;
|
||||||
if (count == 0 || off + payload > raw.size())
|
if (count == 0 || off + payload > raw.size()) {
|
||||||
{
|
|
||||||
break; // truncated or garbled -> stop
|
break; // truncated or garbled -> stop
|
||||||
}
|
}
|
||||||
cap.counts.push_back(count);
|
cap.counts.push_back(count);
|
||||||
@@ -97,14 +86,12 @@ ChunkedCapture parse_chunks(const std::vector<BYTE>& raw, unsigned stride)
|
|||||||
FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsigned window_ms, bool recover_layout)
|
FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsigned window_ms, bool recover_layout)
|
||||||
{
|
{
|
||||||
FormatVerification result;
|
FormatVerification result;
|
||||||
if (ring == nullptr)
|
if (ring == nullptr) {
|
||||||
{
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
WAVEFORMATEX* dev_wfx = default_render_format();
|
WAVEFORMATEX* dev_wfx = default_render_format();
|
||||||
if (dev_wfx == nullptr)
|
if (dev_wfx == nullptr) {
|
||||||
{
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
const ScalarFormat dev = resolve(dev_wfx);
|
const ScalarFormat dev = resolve(dev_wfx);
|
||||||
@@ -120,12 +107,10 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign
|
|||||||
ProcessLoopbackCapture loop;
|
ProcessLoopbackCapture loop;
|
||||||
const std::uint32_t loop_block = dev_wfx->nBlockAlign;
|
const std::uint32_t loop_block = dev_wfx->nBlockAlign;
|
||||||
if (!loop.start(pid, dev_wfx, [&](const BYTE* data, std::uint32_t frames, bool silent) {
|
if (!loop.start(pid, dev_wfx, [&](const BYTE* data, std::uint32_t frames, bool silent) {
|
||||||
if (!silent && data != nullptr)
|
if (!silent && data != nullptr) {
|
||||||
{
|
|
||||||
loop_bytes.insert(loop_bytes.end(), data, data + static_cast<std::size_t>(frames) * loop_block);
|
loop_bytes.insert(loop_bytes.end(), data, data + static_cast<std::size_t>(frames) * loop_block);
|
||||||
}
|
}
|
||||||
}))
|
})) {
|
||||||
{
|
|
||||||
// Distinguish "couldn't activate process loopback" from "captured fine but didn't correlate":
|
// Distinguish "couldn't activate process loopback" from "captured fine but didn't correlate":
|
||||||
// without the ground-truth post-mix path there's nothing to correlate against, so bail now
|
// without the ground-truth post-mix path there's nothing to correlate against, so bail now
|
||||||
// (don't burn the window capturing only the hook side) and leave the diagnostic visible.
|
// (don't burn the window capturing only the hook side) and leave the diagnostic visible.
|
||||||
@@ -137,18 +122,15 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign
|
|||||||
// Pull the hook's pre-mix bytes out of the ring across the window.
|
// Pull the hook's pre-mix bytes out of the ring across the window.
|
||||||
std::vector<BYTE> hook_bytes;
|
std::vector<BYTE> hook_bytes;
|
||||||
const DWORD end = GetTickCount() + window_ms;
|
const DWORD end = GetTickCount() + window_ms;
|
||||||
while (GetTickCount() < end)
|
while (GetTickCount() < end) {
|
||||||
{
|
|
||||||
std::uint32_t n = 0;
|
std::uint32_t n = 0;
|
||||||
while ((n = audio_ring_pop(*ring, scratch.data(), static_cast<std::uint32_t>(scratch.size()))) > 0)
|
while ((n = audio_ring_pop(*ring, scratch.data(), static_cast<std::uint32_t>(scratch.size()))) > 0) {
|
||||||
{
|
|
||||||
hook_bytes.insert(hook_bytes.end(), scratch.data(), scratch.data() + n);
|
hook_bytes.insert(hook_bytes.end(), scratch.data(), scratch.data() + n);
|
||||||
}
|
}
|
||||||
Sleep(10);
|
Sleep(10);
|
||||||
}
|
}
|
||||||
std::uint32_t n = 0;
|
std::uint32_t n = 0;
|
||||||
while ((n = audio_ring_pop(*ring, scratch.data(), static_cast<std::uint32_t>(scratch.size()))) > 0)
|
while ((n = audio_ring_pop(*ring, scratch.data(), static_cast<std::uint32_t>(scratch.size()))) > 0) {
|
||||||
{
|
|
||||||
hook_bytes.insert(hook_bytes.end(), scratch.data(), scratch.data() + n);
|
hook_bytes.insert(hook_bytes.end(), scratch.data(), scratch.data() + n);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,19 +148,16 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign
|
|||||||
CoTaskMemFree(dev_wfx);
|
CoTaskMemFree(dev_wfx);
|
||||||
|
|
||||||
char dbg[2] = {};
|
char dbg[2] = {};
|
||||||
if (GetEnvironmentVariableA("COOP_VERIFY_DEBUG", dbg, sizeof(dbg)) > 0 && dbg[0] == '1')
|
if (GetEnvironmentVariableA("COOP_VERIFY_DEBUG", dbg, sizeof(dbg)) > 0 && dbg[0] == '1') {
|
||||||
{
|
|
||||||
std::fprintf(stderr, "[verify] dev=%uHz/%uch/%ubit blk=%u chunks=%zu hook_frames=%zu loop=%zu layout=%d\n",
|
std::fprintf(stderr, "[verify] dev=%uHz/%uch/%ubit blk=%u chunks=%zu hook_frames=%zu loop=%zu layout=%d\n",
|
||||||
dev.rate, dev.channels, dev.bits, dev_block, cap.counts.size(), hook_frames, loop_mono.size(),
|
dev.rate, dev.channels, dev.bits, dev_block, cap.counts.size(), hook_frames, loop_mono.size(),
|
||||||
recover_layout ? 1 : 0);
|
recover_layout ? 1 : 0);
|
||||||
}
|
}
|
||||||
if (loop_mono.size() < need || hook_frames < need)
|
if (loop_mono.size() < need || hook_frames < need) {
|
||||||
{
|
|
||||||
return result; // not enough non-silent audio captured (game quiet, or stream wasn't a guess)
|
return result; // not enough non-silent audio captured (game quiet, or stream wasn't a guess)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recover_layout)
|
if (recover_layout) {
|
||||||
{
|
|
||||||
// Recover channels + bit depth too, by trying candidate de-interleavings of the
|
// Recover channels + bit depth too, by trying candidate de-interleavings of the
|
||||||
// (de-padded) hook bytes and keeping whichever (layout, rate) correlates with the loopback.
|
// (de-padded) hook bytes and keeping whichever (layout, rate) correlates with the loopback.
|
||||||
const FormatCorrelation fc =
|
const FormatCorrelation fc =
|
||||||
@@ -190,9 +169,7 @@ FormatVerification verify_stream_format(DWORD pid, AudioRingHeader* ring, unsign
|
|||||||
result.channels = fc.channels;
|
result.channels = fc.channels;
|
||||||
result.bits = fc.bits;
|
result.bits = fc.bits;
|
||||||
result.format_tag = fc.tag;
|
result.format_tag = fc.tag;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
// Rate only, assuming the hook layout matches the device (common stereo case), so
|
// Rate only, assuming the hook layout matches the device (common stereo case), so
|
||||||
// the de-padded payload is already clean device-layout audio.
|
// the de-padded payload is already clean device-layout audio.
|
||||||
const std::vector<float> hook_mono = to_mono(cap.bytes, dev);
|
const std::vector<float> hook_mono = to_mono(cap.bytes, dev);
|
||||||
|
|||||||
@@ -20,11 +20,9 @@
|
|||||||
|
|
||||||
#include "coop/audio_ring.hpp"
|
#include "coop/audio_ring.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
struct FormatVerification
|
struct FormatVerification {
|
||||||
{
|
|
||||||
bool ok = false; // a confident rate correlation was found
|
bool ok = false; // a confident rate correlation was found
|
||||||
unsigned rate = 0; // recovered true sample rate (Hz)
|
unsigned rate = 0; // recovered true sample rate (Hz)
|
||||||
double score = 0.0; // correlation score of the winning rate, [0,1]
|
double score = 0.0; // correlation score of the winning rate, [0,1]
|
||||||
|
|||||||
@@ -14,15 +14,12 @@
|
|||||||
#include "audio/process_loopback_capture.hpp"
|
#include "audio/process_loopback_capture.hpp"
|
||||||
#include "audio/render_pacer.hpp"
|
#include "audio/render_pacer.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
namespace {
|
||||||
namespace
|
|
||||||
{
|
|
||||||
|
|
||||||
// Single-producer/single-consumer byte FIFO guarded by a mutex (the capture
|
// Single-producer/single-consumer byte FIFO guarded by a mutex (the capture
|
||||||
// thread pushes, the render thread pops). Overflow drops the oldest samples.
|
// thread pushes, the render thread pops). Overflow drops the oldest samples.
|
||||||
struct ByteRing
|
struct ByteRing {
|
||||||
{
|
|
||||||
std::mutex mutex;
|
std::mutex mutex;
|
||||||
std::vector<BYTE> buf;
|
std::vector<BYTE> buf;
|
||||||
size_t head = 0;
|
size_t head = 0;
|
||||||
@@ -37,8 +34,7 @@ struct ByteRing
|
|||||||
|
|
||||||
void drop_for(size_t incoming)
|
void drop_for(size_t incoming)
|
||||||
{
|
{
|
||||||
if (count + incoming > buf.size())
|
if (count + incoming > buf.size()) {
|
||||||
{
|
|
||||||
const size_t drop = count + incoming - buf.size();
|
const size_t drop = count + incoming - buf.size();
|
||||||
head = (head + drop) % buf.size();
|
head = (head + drop) % buf.size();
|
||||||
count -= drop;
|
count -= drop;
|
||||||
@@ -48,10 +44,8 @@ struct ByteRing
|
|||||||
void push(const BYTE* data, size_t bytes, bool silent)
|
void push(const BYTE* data, size_t bytes, bool silent)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mutex);
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
if (bytes > buf.size())
|
if (bytes > buf.size()) {
|
||||||
{
|
if (data) {
|
||||||
if (data)
|
|
||||||
{
|
|
||||||
data += bytes - buf.size();
|
data += bytes - buf.size();
|
||||||
}
|
}
|
||||||
bytes = buf.size();
|
bytes = buf.size();
|
||||||
@@ -59,29 +53,21 @@ struct ByteRing
|
|||||||
drop_for(bytes);
|
drop_for(bytes);
|
||||||
const size_t tail = (head + count) % buf.size();
|
const size_t tail = (head + count) % buf.size();
|
||||||
const size_t first = std::min(bytes, buf.size() - tail);
|
const size_t first = std::min(bytes, buf.size() - tail);
|
||||||
if (silent || !data)
|
if (silent || !data) {
|
||||||
{
|
|
||||||
std::memset(&buf[tail], 0, first);
|
std::memset(&buf[tail], 0, first);
|
||||||
if (bytes > first)
|
if (bytes > first) {
|
||||||
{
|
|
||||||
std::memset(&buf[0], 0, bytes - first);
|
std::memset(&buf[0], 0, bytes - first);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
std::memcpy(&buf[tail], data, first);
|
std::memcpy(&buf[tail], data, first);
|
||||||
if (bytes > first)
|
if (bytes > first) {
|
||||||
{
|
|
||||||
std::memcpy(&buf[0], data + first, bytes - first);
|
std::memcpy(&buf[0], data + first, bytes - first);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
count += bytes;
|
count += bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t available() const
|
size_t available() const { return count; }
|
||||||
{
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy up to `bytes` into `dst`; returns how many bytes were available.
|
// Copy up to `bytes` into `dst`; returns how many bytes were available.
|
||||||
size_t pop(BYTE* dst, size_t bytes)
|
size_t pop(BYTE* dst, size_t bytes)
|
||||||
@@ -90,8 +76,7 @@ struct ByteRing
|
|||||||
bytes = std::min(bytes, count);
|
bytes = std::min(bytes, count);
|
||||||
const size_t first = std::min(bytes, buf.size() - head);
|
const size_t first = std::min(bytes, buf.size() - head);
|
||||||
std::memcpy(dst, &buf[head], first);
|
std::memcpy(dst, &buf[head], first);
|
||||||
if (bytes > first)
|
if (bytes > first) {
|
||||||
{
|
|
||||||
std::memcpy(dst + first, &buf[0], bytes - first);
|
std::memcpy(dst + first, &buf[0], bytes - first);
|
||||||
}
|
}
|
||||||
head = (head + bytes) % buf.size();
|
head = (head + bytes) % buf.size();
|
||||||
@@ -102,8 +87,7 @@ struct ByteRing
|
|||||||
|
|
||||||
// The default render endpoint plus an event-driven render client, shared by the hooked and
|
// The default render endpoint plus an event-driven render client, shared by the hooked and
|
||||||
// loopback mirror paths. RAII: everything acquired is released on destruction.
|
// loopback mirror paths. RAII: everything acquired is released on destruction.
|
||||||
struct RenderEndpoint
|
struct RenderEndpoint {
|
||||||
{
|
|
||||||
IMMDeviceEnumerator* enumerator = nullptr;
|
IMMDeviceEnumerator* enumerator = nullptr;
|
||||||
IMMDevice* endpoint = nullptr;
|
IMMDevice* endpoint = nullptr;
|
||||||
IAudioClient* client = nullptr;
|
IAudioClient* client = nullptr;
|
||||||
@@ -117,24 +101,19 @@ struct RenderEndpoint
|
|||||||
|
|
||||||
~RenderEndpoint()
|
~RenderEndpoint()
|
||||||
{
|
{
|
||||||
if (render)
|
if (render) {
|
||||||
{
|
|
||||||
render->Release();
|
render->Release();
|
||||||
}
|
}
|
||||||
if (client)
|
if (client) {
|
||||||
{
|
|
||||||
client->Release();
|
client->Release();
|
||||||
}
|
}
|
||||||
if (endpoint)
|
if (endpoint) {
|
||||||
{
|
|
||||||
endpoint->Release();
|
endpoint->Release();
|
||||||
}
|
}
|
||||||
if (enumerator)
|
if (enumerator) {
|
||||||
{
|
|
||||||
enumerator->Release();
|
enumerator->Release();
|
||||||
}
|
}
|
||||||
if (event)
|
if (event) {
|
||||||
{
|
|
||||||
CloseHandle(event);
|
CloseHandle(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,28 +123,24 @@ struct RenderEndpoint
|
|||||||
template <typename Fail>
|
template <typename Fail>
|
||||||
bool activate(Fail&& fail)
|
bool activate(Fail&& fail)
|
||||||
{
|
{
|
||||||
HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
|
HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator),
|
||||||
__uuidof(IMMDeviceEnumerator), reinterpret_cast<void**>(&enumerator));
|
reinterpret_cast<void**>(&enumerator));
|
||||||
if (FAILED(hr))
|
if (FAILED(hr)) {
|
||||||
{
|
|
||||||
fail("CoCreateInstance(MMDeviceEnumerator)", hr);
|
fail("CoCreateInstance(MMDeviceEnumerator)", hr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint);
|
hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint);
|
||||||
if (FAILED(hr))
|
if (FAILED(hr)) {
|
||||||
{
|
|
||||||
fail("GetDefaultAudioEndpoint", hr);
|
fail("GetDefaultAudioEndpoint", hr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
hr = endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast<void**>(&client));
|
hr = endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast<void**>(&client));
|
||||||
if (FAILED(hr))
|
if (FAILED(hr)) {
|
||||||
{
|
|
||||||
fail("Activate render client", hr);
|
fail("Activate render client", hr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
||||||
if (!event)
|
if (!event) {
|
||||||
{
|
|
||||||
fail("CreateEvent(render)", HRESULT_FROM_WIN32(GetLastError()));
|
fail("CreateEvent(render)", HRESULT_FROM_WIN32(GetLastError()));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -185,20 +160,17 @@ struct RenderEndpoint
|
|||||||
bool wire(Fail&& fail)
|
bool wire(Fail&& fail)
|
||||||
{
|
{
|
||||||
HRESULT hr = client->SetEventHandle(event);
|
HRESULT hr = client->SetEventHandle(event);
|
||||||
if (FAILED(hr))
|
if (FAILED(hr)) {
|
||||||
{
|
|
||||||
fail("Render SetEventHandle", hr);
|
fail("Render SetEventHandle", hr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
hr = client->GetService(__uuidof(IAudioRenderClient), reinterpret_cast<void**>(&render));
|
hr = client->GetService(__uuidof(IAudioRenderClient), reinterpret_cast<void**>(&render));
|
||||||
if (FAILED(hr))
|
if (FAILED(hr)) {
|
||||||
{
|
|
||||||
fail("GetService(RenderClient)", hr);
|
fail("GetService(RenderClient)", hr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
hr = client->GetBufferSize(&buffer_frames);
|
hr = client->GetBufferSize(&buffer_frames);
|
||||||
if (FAILED(hr))
|
if (FAILED(hr)) {
|
||||||
{
|
|
||||||
fail("GetBufferSize", hr);
|
fail("GetBufferSize", hr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -246,10 +218,8 @@ void AudioMirror::set_fallback_reason(std::string s)
|
|||||||
|
|
||||||
void AudioMirror::enable_capture(AudioRingHeader* const* rings, bool on)
|
void AudioMirror::enable_capture(AudioRingHeader* const* rings, bool on)
|
||||||
{
|
{
|
||||||
for (unsigned i = 0; i < kMaxAudioStreams; ++i)
|
for (unsigned i = 0; i < kMaxAudioStreams; ++i) {
|
||||||
{
|
if (rings[i] != nullptr) {
|
||||||
if (rings[i] != nullptr)
|
|
||||||
{
|
|
||||||
rings[i]->capture_enabled.store(on ? 1u : 0u, std::memory_order_release);
|
rings[i]->capture_enabled.store(on ? 1u : 0u, std::memory_order_release);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,8 +228,7 @@ void AudioMirror::enable_capture(AudioRingHeader* const* rings, bool on)
|
|||||||
void AudioMirror::request_op(unsigned slot, std::uint32_t kind, std::uint32_t rate, std::uint32_t channels,
|
void AudioMirror::request_op(unsigned slot, std::uint32_t kind, std::uint32_t rate, std::uint32_t channels,
|
||||||
std::uint32_t bits, std::uint32_t format_tag)
|
std::uint32_t bits, std::uint32_t format_tag)
|
||||||
{
|
{
|
||||||
if (slot >= kMaxAudioStreams)
|
if (slot >= kMaxAudioStreams) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::lock_guard<std::mutex> lock(ops_mutex_);
|
std::lock_guard<std::mutex> lock(ops_mutex_);
|
||||||
@@ -273,11 +242,9 @@ void AudioMirror::drain_ops()
|
|||||||
std::lock_guard<std::mutex> lock(ops_mutex_);
|
std::lock_guard<std::mutex> lock(ops_mutex_);
|
||||||
ops.swap(pending_ops_);
|
ops.swap(pending_ops_);
|
||||||
}
|
}
|
||||||
for (const PendingOp& op : ops)
|
for (const PendingOp& op : ops) {
|
||||||
{
|
|
||||||
AudioRingHeader* ring = (op.slot < kMaxAudioStreams) ? session_rings_[op.slot] : nullptr;
|
AudioRingHeader* ring = (op.slot < kMaxAudioStreams) ? session_rings_[op.slot] : nullptr;
|
||||||
if (ring != nullptr)
|
if (ring != nullptr) {
|
||||||
{
|
|
||||||
audio_ring_post_op(*ring, op.kind, op.rate, op.channels, op.bits, op.format_tag);
|
audio_ring_post_op(*ring, op.kind, op.rate, op.channels, op.bits, op.format_tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,14 +253,12 @@ void AudioMirror::drain_ops()
|
|||||||
bool AudioMirror::start(DWORD pid)
|
bool AudioMirror::start(DWORD pid)
|
||||||
{
|
{
|
||||||
stop();
|
stop();
|
||||||
if (!pid)
|
if (!pid) {
|
||||||
{
|
|
||||||
set_status("No target process.");
|
set_status("No target process.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
stop_event_ = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
stop_event_ = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||||
if (!stop_event_)
|
if (!stop_event_) {
|
||||||
{
|
|
||||||
set_status("CreateEvent failed.");
|
set_status("CreateEvent failed.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -305,21 +270,17 @@ bool AudioMirror::start(DWORD pid)
|
|||||||
|
|
||||||
void AudioMirror::stop()
|
void AudioMirror::stop()
|
||||||
{
|
{
|
||||||
if (stop_event_)
|
if (stop_event_) {
|
||||||
{
|
|
||||||
SetEvent(stop_event_);
|
SetEvent(stop_event_);
|
||||||
}
|
}
|
||||||
if (thread_.joinable())
|
if (thread_.joinable()) {
|
||||||
{
|
|
||||||
thread_.join();
|
thread_.join();
|
||||||
}
|
}
|
||||||
if (stop_event_)
|
if (stop_event_) {
|
||||||
{
|
|
||||||
CloseHandle(stop_event_);
|
CloseHandle(stop_event_);
|
||||||
stop_event_ = nullptr;
|
stop_event_ = nullptr;
|
||||||
}
|
}
|
||||||
for (auto& shm : audio_ring_shm_)
|
for (auto& shm : audio_ring_shm_) {
|
||||||
{
|
|
||||||
shm.reset();
|
shm.reset();
|
||||||
}
|
}
|
||||||
running_.store(false, std::memory_order_release);
|
running_.store(false, std::memory_order_release);
|
||||||
@@ -337,18 +298,14 @@ bool AudioMirror::stop_requested() const
|
|||||||
bool AudioMirror::wait_for_format(AudioRingHeader* ring, DWORD timeout_ms)
|
bool AudioMirror::wait_for_format(AudioRingHeader* ring, DWORD timeout_ms)
|
||||||
{
|
{
|
||||||
const DWORD end = GetTickCount() + timeout_ms;
|
const DWORD end = GetTickCount() + timeout_ms;
|
||||||
for (;;)
|
for (;;) {
|
||||||
{
|
if (audio_ring_format_ready(*ring)) {
|
||||||
if (audio_ring_format_ready(*ring))
|
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (stop_event_ && WaitForSingleObject(stop_event_, 25) == WAIT_OBJECT_0)
|
if (stop_event_ && WaitForSingleObject(stop_event_, 25) == WAIT_OBJECT_0) {
|
||||||
{
|
|
||||||
return false; // stopping
|
return false; // stopping
|
||||||
}
|
}
|
||||||
if (GetTickCount() >= end)
|
if (GetTickCount() >= end) {
|
||||||
{
|
|
||||||
return false; // hook never published a format -> fall back to loopback
|
return false; // hook never published a format -> fall back to loopback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,10 +320,8 @@ void AudioMirror::thread_main(DWORD pid)
|
|||||||
// live the whole session and promote loopback -> hooked the moment a format appears.
|
// live the whole session and promote loopback -> hooked the moment a format appears.
|
||||||
AudioRingHeader* rings[kMaxAudioStreams] = {};
|
AudioRingHeader* rings[kMaxAudioStreams] = {};
|
||||||
bool created_primary = false;
|
bool created_primary = false;
|
||||||
for (unsigned i = 0; i < kMaxAudioStreams; ++i)
|
for (unsigned i = 0; i < kMaxAudioStreams; ++i) {
|
||||||
{
|
if (audio_ring_shm_[i].create(audio_ring_name(pid, i), audio_ring_total_size(kAudioRingCapacity))) {
|
||||||
if (audio_ring_shm_[i].create(audio_ring_name(pid, i), audio_ring_total_size(kAudioRingCapacity)))
|
|
||||||
{
|
|
||||||
rings[i] = audio_ring_shm_[i].as<AudioRingHeader>();
|
rings[i] = audio_ring_shm_[i].as<AudioRingHeader>();
|
||||||
audio_ring_init(*rings[i], kAudioRingCapacity);
|
audio_ring_init(*rings[i], kAudioRingCapacity);
|
||||||
created_primary = created_primary || (i == 0);
|
created_primary = created_primary || (i == 0);
|
||||||
@@ -374,17 +329,13 @@ void AudioMirror::thread_main(DWORD pid)
|
|||||||
session_rings_[i] = rings[i]; // visible to drain_ops on this (audio) thread
|
session_rings_[i] = rings[i]; // visible to drain_ops on this (audio) thread
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!created_primary)
|
if (!created_primary) {
|
||||||
{
|
|
||||||
// Couldn't create the hook's ring -> loopback only (no promote target).
|
// Couldn't create the hook's ring -> loopback only (no promote target).
|
||||||
set_fallback_reason("Couldn't create the audio ring; using loopback (echo).");
|
set_fallback_reason("Couldn't create the audio ring; using loopback (echo).");
|
||||||
if (!stop_requested())
|
if (!stop_requested()) {
|
||||||
{
|
|
||||||
run_loopback(pid, nullptr);
|
run_loopback(pid, nullptr);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
// Prefer the hooked (no-echo) path. While it isn't ready, run loopback (echo) so
|
// Prefer the hooked (no-echo) path. While it isn't ready, run loopback (echo) so
|
||||||
// guests still hear audio, but watch the ring and promote to hooked the instant the
|
// guests still hear audio, but watch the ring and promote to hooked the instant the
|
||||||
// hook publishes a format. A short wait first catches the fast cases (exact format /
|
// hook publishes a format. A short wait first catches the fast cases (exact format /
|
||||||
@@ -393,37 +344,29 @@ void AudioMirror::thread_main(DWORD pid)
|
|||||||
// that gap and the promote hands off seamlessly.
|
// that gap and the promote hands off seamlessly.
|
||||||
constexpr DWORD kHookWaitMs = 1200;
|
constexpr DWORD kHookWaitMs = 1200;
|
||||||
bool format_verified = false; // run the two-path correlation verify/correct at most once
|
bool format_verified = false; // run the two-path correlation verify/correct at most once
|
||||||
for (;;)
|
for (;;) {
|
||||||
{
|
if (stop_requested()) {
|
||||||
if (stop_requested())
|
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
set_status("Waiting for render-hook…");
|
set_status("Waiting for render-hook…");
|
||||||
bool watch_for_promote = true;
|
bool watch_for_promote = true;
|
||||||
if (wait_for_format(rings[0], kHookWaitMs))
|
if (wait_for_format(rings[0], kHookWaitMs)) {
|
||||||
{
|
|
||||||
set_fallback_reason({}); // hooked path is taking over
|
set_fallback_reason({}); // hooked path is taking over
|
||||||
const HookedResult r = run_hooked(rings);
|
const HookedResult r = run_hooked(rings);
|
||||||
if (r == HookedResult::Stopped)
|
if (r == HookedResult::Stopped) {
|
||||||
{
|
|
||||||
break; // ran to a clean stop
|
break; // ran to a clean stop
|
||||||
}
|
}
|
||||||
if (r == HookedResult::Reinit)
|
if (r == HookedResult::Reinit) {
|
||||||
{
|
|
||||||
continue; // hook re-published (re-measure / override) -> re-read the new format
|
continue; // hook re-published (re-measure / override) -> re-read the new format
|
||||||
}
|
}
|
||||||
if (stop_requested())
|
if (stop_requested()) {
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// run_hooked failed to initialize (the game's format isn't renderable here).
|
// run_hooked failed to initialize (the game's format isn't renderable here).
|
||||||
// That won't fix itself, so don't bounce back to it -- stay on loopback.
|
// That won't fix itself, so don't bounce back to it -- stay on loopback.
|
||||||
set_fallback_reason("Render-hook format isn't renderable on this endpoint; using loopback (echo).");
|
set_fallback_reason("Render-hook format isn't renderable on this endpoint; using loopback (echo).");
|
||||||
watch_for_promote = false;
|
watch_for_promote = false;
|
||||||
}
|
} else if (!stop_requested()) {
|
||||||
else if (!stop_requested())
|
|
||||||
{
|
|
||||||
set_fallback_reason(
|
set_fallback_reason(
|
||||||
"Render-hook hasn't published a format yet; using loopback (echo) -- will switch to "
|
"Render-hook hasn't published a format yet; using loopback (echo) -- will switch to "
|
||||||
"hooked automatically once it does.");
|
"hooked automatically once it does.");
|
||||||
@@ -433,8 +376,7 @@ void AudioMirror::thread_main(DWORD pid)
|
|||||||
// and correct it through the existing override channel -- this hardens the cadence
|
// and correct it through the existing override channel -- this hardens the cadence
|
||||||
// method's intermittent pitch-shift. It's hidden inside the measurement gap loopback
|
// method's intermittent pitch-shift. It's hidden inside the measurement gap loopback
|
||||||
// already covers, so exact streams (format published immediately) never pay for it.
|
// already covers, so exact streams (format published immediately) never pay for it.
|
||||||
if (!format_verified)
|
if (!format_verified) {
|
||||||
{
|
|
||||||
format_verified = true;
|
format_verified = true;
|
||||||
// recover_layout: correlate the full format (rate AND channels/bit-depth), so a
|
// recover_layout: correlate the full format (rate AND channels/bit-depth), so a
|
||||||
// game rendering a different layout than the device is corrected too, not just
|
// game rendering a different layout than the device is corrected too, not just
|
||||||
@@ -442,8 +384,7 @@ void AudioMirror::thread_main(DWORD pid)
|
|||||||
// a silent game, or a genuinely ambiguous identical-channel layout).
|
// a silent game, or a genuinely ambiguous identical-channel layout).
|
||||||
const FormatVerification fv = verify_stream_format(pid, rings[0], /*window_ms=*/900,
|
const FormatVerification fv = verify_stream_format(pid, rings[0], /*window_ms=*/900,
|
||||||
/*recover_layout=*/true);
|
/*recover_layout=*/true);
|
||||||
if (fv.ok)
|
if (fv.ok) {
|
||||||
{
|
|
||||||
set_status("Verified render-hook format by correlation.");
|
set_status("Verified render-hook format by correlation.");
|
||||||
audio_ring_post_op(*rings[0], AudioRingOp_Override, fv.rate, fv.channels, fv.bits,
|
audio_ring_post_op(*rings[0], AudioRingOp_Override, fv.rate, fv.channels, fv.bits,
|
||||||
fv.format_tag);
|
fv.format_tag);
|
||||||
@@ -452,8 +393,7 @@ void AudioMirror::thread_main(DWORD pid)
|
|||||||
}
|
}
|
||||||
|
|
||||||
enable_capture(rings, false); // game audible locally so loopback can capture it
|
enable_capture(rings, false); // game audible locally so loopback can capture it
|
||||||
if (!run_loopback(pid, watch_for_promote ? rings[0] : nullptr))
|
if (!run_loopback(pid, watch_for_promote ? rings[0] : nullptr)) {
|
||||||
{
|
|
||||||
break; // stopped (not a promote)
|
break; // stopped (not a promote)
|
||||||
}
|
}
|
||||||
// Promoted: a format appeared -> loop and try the hooked path again.
|
// Promoted: a format appeared -> loop and try the hooked path again.
|
||||||
@@ -461,24 +401,20 @@ void AudioMirror::thread_main(DWORD pid)
|
|||||||
}
|
}
|
||||||
|
|
||||||
enable_capture(rings, false);
|
enable_capture(rings, false);
|
||||||
for (unsigned i = 0; i < kMaxAudioStreams; ++i)
|
for (unsigned i = 0; i < kMaxAudioStreams; ++i) {
|
||||||
{
|
|
||||||
session_rings_[i] = nullptr; // audio thread owns this; cleared before unmapping
|
session_rings_[i] = nullptr; // audio thread owns this; cleared before unmapping
|
||||||
}
|
}
|
||||||
for (auto& shm : audio_ring_shm_)
|
for (auto& shm : audio_ring_shm_) {
|
||||||
{
|
|
||||||
shm.reset();
|
shm.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (running_.load(std::memory_order_acquire))
|
if (running_.load(std::memory_order_acquire)) {
|
||||||
{
|
|
||||||
running_.store(false, std::memory_order_release);
|
running_.store(false, std::memory_order_release);
|
||||||
set_status("Stopped.");
|
set_status("Stopped.");
|
||||||
}
|
}
|
||||||
source_.store(Source::None, std::memory_order_relaxed);
|
source_.store(Source::None, std::memory_order_relaxed);
|
||||||
|
|
||||||
if (com_ok)
|
if (com_ok) {
|
||||||
{
|
|
||||||
CoUninitialize();
|
CoUninitialize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,8 +437,7 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings)
|
|||||||
const unsigned bits = primary->bits;
|
const unsigned bits = primary->bits;
|
||||||
const unsigned tag = primary->format_tag;
|
const unsigned tag = primary->format_tag;
|
||||||
const unsigned block_align = primary->block_align ? primary->block_align : channels * (bits / 8);
|
const unsigned block_align = primary->block_align ? primary->block_align : channels * (bits / 8);
|
||||||
if (rate == 0 || channels == 0 || block_align == 0)
|
if (rate == 0 || channels == 0 || block_align == 0) {
|
||||||
{
|
|
||||||
enable_capture(rings, false); // let the game play locally again
|
enable_capture(rings, false); // let the game play locally again
|
||||||
return HookedResult::Failed;
|
return HookedResult::Failed;
|
||||||
}
|
}
|
||||||
@@ -515,8 +450,7 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings)
|
|||||||
wfx.Format.wBitsPerSample = static_cast<WORD>(bits);
|
wfx.Format.wBitsPerSample = static_cast<WORD>(bits);
|
||||||
wfx.Format.nBlockAlign = static_cast<WORD>(block_align);
|
wfx.Format.nBlockAlign = static_cast<WORD>(block_align);
|
||||||
wfx.Format.nAvgBytesPerSec = block_align * rate;
|
wfx.Format.nAvgBytesPerSec = block_align * rate;
|
||||||
if (channels > 2 || bits > 16)
|
if (channels > 2 || bits > 16) {
|
||||||
{
|
|
||||||
wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
|
wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
|
||||||
wfx.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
|
wfx.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
|
||||||
wfx.Samples.wValidBitsPerSample = static_cast<WORD>(bits);
|
wfx.Samples.wValidBitsPerSample = static_cast<WORD>(bits);
|
||||||
@@ -532,11 +466,8 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings)
|
|||||||
wfx.dwChannelMask = (channels >= 32) ? 0xFFFFFFFFu : ((1u << channels) - 1u);
|
wfx.dwChannelMask = (channels >= 32) ? 0xFFFFFFFFu : ((1u << channels) - 1u);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
wfx.SubFormat =
|
wfx.SubFormat = (tag == WAVE_FORMAT_IEEE_FLOAT) ? KSDATAFORMAT_SUBTYPE_IEEE_FLOAT : KSDATAFORMAT_SUBTYPE_PCM;
|
||||||
(tag == WAVE_FORMAT_IEEE_FLOAT) ? KSDATAFORMAT_SUBTYPE_IEEE_FLOAT : KSDATAFORMAT_SUBTYPE_PCM;
|
} else {
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
wfx.Format.wFormatTag = static_cast<WORD>(tag ? tag : WAVE_FORMAT_PCM);
|
wfx.Format.wFormatTag = static_cast<WORD>(tag ? tag : WAVE_FORMAT_PCM);
|
||||||
wfx.Format.cbSize = 0;
|
wfx.Format.cbSize = 0;
|
||||||
}
|
}
|
||||||
@@ -547,22 +478,18 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings)
|
|||||||
HookedResult result = HookedResult::Stopped;
|
HookedResult result = HookedResult::Stopped;
|
||||||
auto fail = [this](const char* step, HRESULT hr) { set_error(step, hr); };
|
auto fail = [this](const char* step, HRESULT hr) { set_error(step, hr); };
|
||||||
|
|
||||||
do
|
do {
|
||||||
{
|
if (!ep.activate(fail)) {
|
||||||
if (!ep.activate(fail))
|
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM |
|
const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM
|
||||||
AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
|
| AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
|
||||||
if (FAILED(ep.initialize(fmt, flags)))
|
if (FAILED(ep.initialize(fmt, flags))) {
|
||||||
{
|
|
||||||
// The game's format isn't renderable here (rare). Bail to loopback.
|
// The game's format isn't renderable here (rare). Bail to loopback.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
started = true; // past the point where falling back is clean
|
started = true; // past the point where falling back is clean
|
||||||
if (!ep.wire(fail))
|
if (!ep.wire(fail)) {
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
IAudioClient* render_client = ep.client;
|
IAudioClient* render_client = ep.client;
|
||||||
@@ -582,8 +509,7 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings)
|
|||||||
std::vector<BYTE> temp(static_cast<size_t>(render_frames) * frame_bytes);
|
std::vector<BYTE> temp(static_cast<size_t>(render_frames) * frame_bytes);
|
||||||
std::vector<float> acc(static_cast<size_t>(render_frames) * channels);
|
std::vector<float> acc(static_cast<size_t>(render_frames) * channels);
|
||||||
|
|
||||||
if (const HRESULT hr = render_client->Start(); FAILED(hr))
|
if (const HRESULT hr = render_client->Start(); FAILED(hr)) {
|
||||||
{
|
|
||||||
set_error("Render Start", hr);
|
set_error("Render Start", hr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -595,23 +521,19 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings)
|
|||||||
running_.store(true, std::memory_order_release);
|
running_.store(true, std::memory_order_release);
|
||||||
|
|
||||||
HANDLE waits[2] = {stop_event_, ep.event};
|
HANDLE waits[2] = {stop_event_, ep.event};
|
||||||
for (;;)
|
for (;;) {
|
||||||
{
|
|
||||||
const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200);
|
const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200);
|
||||||
if (w == WAIT_OBJECT_0)
|
if (w == WAIT_OBJECT_0) {
|
||||||
{
|
|
||||||
break; // stop requested
|
break; // stop requested
|
||||||
}
|
}
|
||||||
drain_ops(); // post any queued operator ops (re-measure / override) to the hook
|
drain_ops(); // post any queued operator ops (re-measure / override) to the hook
|
||||||
if (primary->format_generation.load(std::memory_order_acquire) != start_gen)
|
if (primary->format_generation.load(std::memory_order_acquire) != start_gen) {
|
||||||
{
|
|
||||||
result = HookedResult::Reinit; // hook re-published -> re-read the new format
|
result = HookedResult::Reinit; // hook re-published -> re-read the new format
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
UINT32 padding = 0;
|
UINT32 padding = 0;
|
||||||
if (FAILED(render_client->GetCurrentPadding(&padding)))
|
if (FAILED(render_client->GetCurrentPadding(&padding))) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const UINT32 avail = render_frames - padding;
|
const UINT32 avail = render_frames - padding;
|
||||||
@@ -622,43 +544,35 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings)
|
|||||||
const UINT32 have = static_cast<UINT32>(ring_bytes / frame_bytes);
|
const UINT32 have = static_cast<UINT32>(ring_bytes / frame_bytes);
|
||||||
const UINT32 to_write = pacer.pump(avail, have, padding);
|
const UINT32 to_write = pacer.pump(avail, have, padding);
|
||||||
{
|
{
|
||||||
if (to_write > 0)
|
if (to_write > 0) {
|
||||||
{
|
|
||||||
// Active streams = same format as primary (so they can be summed).
|
// Active streams = same format as primary (so they can be summed).
|
||||||
// Streams with a different format are still silenced by the hook (no
|
// Streams with a different format are still silenced by the hook (no
|
||||||
// echo) but can't be mixed here without resampling -> skipped.
|
// echo) but can't be mixed here without resampling -> skipped.
|
||||||
unsigned active[kMaxAudioStreams];
|
unsigned active[kMaxAudioStreams];
|
||||||
unsigned n_active = 0;
|
unsigned n_active = 0;
|
||||||
for (unsigned i = 0; i < kMaxAudioStreams; ++i)
|
for (unsigned i = 0; i < kMaxAudioStreams; ++i) {
|
||||||
{
|
|
||||||
AudioRingHeader* r = rings[i];
|
AudioRingHeader* r = rings[i];
|
||||||
if (r == nullptr)
|
if (r == nullptr) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (i == 0 || (audio_ring_format_ready(*r) && r->sample_rate == rate &&
|
if (i == 0
|
||||||
r->channels == channels && r->bits == bits && r->format_tag == tag))
|
|| (audio_ring_format_ready(*r) && r->sample_rate == rate && r->channels == channels
|
||||||
{
|
&& r->bits == bits && r->format_tag == tag)) {
|
||||||
active[n_active++] = i;
|
active[n_active++] = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BYTE* dst = nullptr;
|
BYTE* dst = nullptr;
|
||||||
if (SUCCEEDED(render->GetBuffer(to_write, &dst)))
|
if (SUCCEEDED(render->GetBuffer(to_write, &dst))) {
|
||||||
{
|
|
||||||
const std::uint32_t want_bytes = to_write * static_cast<std::uint32_t>(frame_bytes);
|
const std::uint32_t want_bytes = to_write * static_cast<std::uint32_t>(frame_bytes);
|
||||||
if (n_active <= 1 || !mixer_ok)
|
if (n_active <= 1 || !mixer_ok) {
|
||||||
{
|
|
||||||
// Single stream (the common case) or an unmixable format:
|
// Single stream (the common case) or an unmixable format:
|
||||||
// passthrough the primary, byte-for-byte (no mixer overhead).
|
// passthrough the primary, byte-for-byte (no mixer overhead).
|
||||||
audio_ring_pop(*primary, dst, want_bytes);
|
audio_ring_pop(*primary, dst, want_bytes);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
const std::uint32_t samples = to_write * channels;
|
const std::uint32_t samples = to_write * channels;
|
||||||
std::fill(acc.begin(), acc.begin() + samples, 0.0f);
|
std::fill(acc.begin(), acc.begin() + samples, 0.0f);
|
||||||
for (unsigned k = 0; k < n_active; ++k)
|
for (unsigned k = 0; k < n_active; ++k) {
|
||||||
{
|
|
||||||
std::memset(temp.data(), 0, want_bytes); // zero-fill short reads
|
std::memset(temp.data(), 0, want_bytes); // zero-fill short reads
|
||||||
audio_ring_pop(*rings[active[k]], temp.data(), want_bytes);
|
audio_ring_pop(*rings[active[k]], temp.data(), want_bytes);
|
||||||
mix_add(acc.data(), temp.data(), samples, tag, bits);
|
mix_add(acc.data(), temp.data(), samples, tag, bits);
|
||||||
@@ -677,13 +591,11 @@ AudioMirror::HookedResult AudioMirror::run_hooked(AudioRingHeader* const* rings)
|
|||||||
|
|
||||||
// On a re-init (format changed) keep capturing so the rebuilt render client picks up
|
// On a re-init (format changed) keep capturing so the rebuilt render client picks up
|
||||||
// seamlessly; otherwise free the game's local playback (stop / fall back to loopback).
|
// seamlessly; otherwise free the game's local playback (stop / fall back to loopback).
|
||||||
if (result != HookedResult::Reinit)
|
if (result != HookedResult::Reinit) {
|
||||||
{
|
|
||||||
enable_capture(rings, false);
|
enable_capture(rings, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!started)
|
if (!started) {
|
||||||
{
|
|
||||||
// Never got a working render client; let the caller try loopback. Capture is
|
// Never got a working render client; let the caller try loopback. Capture is
|
||||||
// already disabled above so loopback hears the game.
|
// already disabled above so loopback hears the game.
|
||||||
return HookedResult::Failed;
|
return HookedResult::Failed;
|
||||||
@@ -701,17 +613,14 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring)
|
|||||||
ProcessLoopbackCapture capture;
|
ProcessLoopbackCapture capture;
|
||||||
auto fail = [this](const char* step, HRESULT hr) { set_error(step, hr); };
|
auto fail = [this](const char* step, HRESULT hr) { set_error(step, hr); };
|
||||||
|
|
||||||
do
|
do {
|
||||||
{
|
if (!ep.activate(fail)) {
|
||||||
if (!ep.activate(fail))
|
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Capture and render share one format (the output endpoint's mix format);
|
// Capture and render share one format (the output endpoint's mix format);
|
||||||
// WASAPI converts the captured process audio into it.
|
// WASAPI converts the captured process audio into it.
|
||||||
HRESULT hr = ep.client->GetMixFormat(&fmt);
|
HRESULT hr = ep.client->GetMixFormat(&fmt);
|
||||||
if (FAILED(hr))
|
if (FAILED(hr)) {
|
||||||
{
|
|
||||||
fail("GetMixFormat", hr);
|
fail("GetMixFormat", hr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -719,13 +628,11 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring)
|
|||||||
channels_.store(fmt->nChannels, std::memory_order_relaxed);
|
channels_.store(fmt->nChannels, std::memory_order_relaxed);
|
||||||
|
|
||||||
hr = ep.initialize(fmt, AUDCLNT_STREAMFLAGS_EVENTCALLBACK);
|
hr = ep.initialize(fmt, AUDCLNT_STREAMFLAGS_EVENTCALLBACK);
|
||||||
if (FAILED(hr))
|
if (FAILED(hr)) {
|
||||||
{
|
|
||||||
fail("Render Initialize", hr);
|
fail("Render Initialize", hr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!ep.wire(fail))
|
if (!ep.wire(fail)) {
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
IAudioClient* render_client = ep.client;
|
IAudioClient* render_client = ep.client;
|
||||||
@@ -743,14 +650,12 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring)
|
|||||||
// Capture pushes packets straight into the render ring.
|
// Capture pushes packets straight into the render ring.
|
||||||
if (!capture.start(pid, fmt, [&ring, frame_bytes](const BYTE* data, UINT32 frames, bool silent) {
|
if (!capture.start(pid, fmt, [&ring, frame_bytes](const BYTE* data, UINT32 frames, bool silent) {
|
||||||
ring.push(data, static_cast<size_t>(frames) * frame_bytes, silent);
|
ring.push(data, static_cast<size_t>(frames) * frame_bytes, silent);
|
||||||
}))
|
})) {
|
||||||
{
|
|
||||||
fail("Capture start", E_FAIL);
|
fail("Capture start", E_FAIL);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FAILED(hr = render_client->Start()))
|
if (FAILED(hr = render_client->Start())) {
|
||||||
{
|
|
||||||
fail("Render Start", hr);
|
fail("Render Start", hr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -762,44 +667,36 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring)
|
|||||||
running_.store(true, std::memory_order_release);
|
running_.store(true, std::memory_order_release);
|
||||||
|
|
||||||
HANDLE waits[2] = {stop_event_, ep.event};
|
HANDLE waits[2] = {stop_event_, ep.event};
|
||||||
for (;;)
|
for (;;) {
|
||||||
{
|
|
||||||
const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200);
|
const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200);
|
||||||
if (w == WAIT_OBJECT_0)
|
if (w == WAIT_OBJECT_0) {
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!capture.running())
|
if (!capture.running()) {
|
||||||
{
|
|
||||||
set_status(capture.status());
|
set_status(capture.status());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
drain_ops(); // operator ops (re-measure / override) reach the hook even on loopback
|
drain_ops(); // operator ops (re-measure / override) reach the hook even on loopback
|
||||||
// Auto-promote: the hook published a format -> hand back so the caller switches
|
// Auto-promote: the hook published a format -> hand back so the caller switches
|
||||||
// to the no-echo hooked path (the rings stayed live the whole time).
|
// to the no-echo hooked path (the rings stayed live the whole time).
|
||||||
if (promote_ring != nullptr && audio_ring_format_ready(*promote_ring))
|
if (promote_ring != nullptr && audio_ring_format_ready(*promote_ring)) {
|
||||||
{
|
|
||||||
set_status("Render-hook ready -- switching to hooked (no echo)…");
|
set_status("Render-hook ready -- switching to hooked (no echo)…");
|
||||||
promote = true;
|
promote = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
UINT32 padding = 0;
|
UINT32 padding = 0;
|
||||||
if (FAILED(render_client->GetCurrentPadding(&padding)))
|
if (FAILED(render_client->GetCurrentPadding(&padding))) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const UINT32 avail = render_frames - padding;
|
const UINT32 avail = render_frames - padding;
|
||||||
buffered_ms_.store(
|
buffered_ms_.store(static_cast<unsigned>(ring.available() / frame_bytes * 1000 / fmt->nSamplesPerSec),
|
||||||
static_cast<unsigned>(ring.available() / frame_bytes * 1000 / fmt->nSamplesPerSec),
|
|
||||||
std::memory_order_relaxed);
|
std::memory_order_relaxed);
|
||||||
const UINT32 have = static_cast<UINT32>(ring.available() / frame_bytes);
|
const UINT32 have = static_cast<UINT32>(ring.available() / frame_bytes);
|
||||||
const UINT32 to_write = pacer.pump(avail, have, padding);
|
const UINT32 to_write = pacer.pump(avail, have, padding);
|
||||||
if (to_write > 0)
|
if (to_write > 0) {
|
||||||
{
|
|
||||||
BYTE* dst = nullptr;
|
BYTE* dst = nullptr;
|
||||||
if (SUCCEEDED(render->GetBuffer(to_write, &dst)))
|
if (SUCCEEDED(render->GetBuffer(to_write, &dst))) {
|
||||||
{
|
|
||||||
ring.pop(dst, static_cast<size_t>(to_write) * frame_bytes);
|
ring.pop(dst, static_cast<size_t>(to_write) * frame_bytes);
|
||||||
render->ReleaseBuffer(to_write, 0);
|
render->ReleaseBuffer(to_write, 0);
|
||||||
}
|
}
|
||||||
@@ -810,8 +707,7 @@ bool AudioMirror::run_loopback(DWORD pid, AudioRingHeader* promote_ring)
|
|||||||
} while (false);
|
} while (false);
|
||||||
|
|
||||||
capture.stop();
|
capture.stop();
|
||||||
if (fmt)
|
if (fmt) {
|
||||||
{
|
|
||||||
CoTaskMemFree(fmt);
|
CoTaskMemFree(fmt);
|
||||||
}
|
}
|
||||||
return promote; // true = hook caught up, caller should switch to hooked
|
return promote; // true = hook caught up, caller should switch to hooked
|
||||||
|
|||||||
@@ -22,11 +22,9 @@
|
|||||||
#include "coop/protocol.hpp" // kMaxAudioStreams
|
#include "coop/protocol.hpp" // kMaxAudioStreams
|
||||||
#include "coop/shared_memory.hpp"
|
#include "coop/shared_memory.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
class AudioMirror
|
class AudioMirror {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
AudioMirror() = default;
|
AudioMirror() = default;
|
||||||
~AudioMirror();
|
~AudioMirror();
|
||||||
@@ -42,49 +40,29 @@ public:
|
|||||||
|
|
||||||
// True once the audio thread is actively mirroring (false while starting or
|
// True once the audio thread is actively mirroring (false while starting or
|
||||||
// after a failure).
|
// after a failure).
|
||||||
[[nodiscard]] bool running() const
|
[[nodiscard]] bool running() const { return running_.load(std::memory_order_acquire); }
|
||||||
{
|
|
||||||
return running_.load(std::memory_order_acquire);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The process currently targeted (0 if stopped). Updated synchronously by
|
// The process currently targeted (0 if stopped). Updated synchronously by
|
||||||
// start()/stop() so the UI can detect target changes without races.
|
// start()/stop() so the UI can detect target changes without races.
|
||||||
[[nodiscard]] DWORD target_pid() const
|
[[nodiscard]] DWORD target_pid() const { return pid_; }
|
||||||
{
|
|
||||||
return pid_;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] unsigned sample_rate() const
|
[[nodiscard]] unsigned sample_rate() const { return sample_rate_.load(std::memory_order_relaxed); }
|
||||||
{
|
[[nodiscard]] unsigned channels() const { return channels_.load(std::memory_order_relaxed); }
|
||||||
return sample_rate_.load(std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
[[nodiscard]] unsigned channels() const
|
|
||||||
{
|
|
||||||
return channels_.load(std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audio currently buffered between capture and the output device, in ms — a
|
// Audio currently buffered between capture and the output device, in ms — a
|
||||||
// health/latency proxy (rises if the consumer can't keep up). 0 when stopped.
|
// health/latency proxy (rises if the consumer can't keep up). 0 when stopped.
|
||||||
[[nodiscard]] unsigned buffered_ms() const
|
[[nodiscard]] unsigned buffered_ms() const { return buffered_ms_.load(std::memory_order_relaxed); }
|
||||||
{
|
|
||||||
return buffered_ms_.load(std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Which capture path is active, for the UI's source indicator.
|
// Which capture path is active, for the UI's source indicator.
|
||||||
enum class Source
|
enum class Source {
|
||||||
{
|
|
||||||
None,
|
None,
|
||||||
Hooked, // shared audio ring from the render-hook (no echo)
|
Hooked, // shared audio ring from the render-hook (no echo)
|
||||||
Loopback, // WASAPI process loopback (echo)
|
Loopback, // WASAPI process loopback (echo)
|
||||||
};
|
};
|
||||||
[[nodiscard]] Source source() const
|
[[nodiscard]] Source source() const { return source_.load(std::memory_order_relaxed); }
|
||||||
{
|
|
||||||
return source_.load(std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
[[nodiscard]] const char* source_name() const
|
[[nodiscard]] const char* source_name() const
|
||||||
{
|
{
|
||||||
switch (source())
|
switch (source()) {
|
||||||
{
|
|
||||||
case Source::Hooked:
|
case Source::Hooked:
|
||||||
return "Hooked"; // echo depends on the format provenance; the panel shows it
|
return "Hooked"; // echo depends on the format provenance; the panel shows it
|
||||||
case Source::Loopback:
|
case Source::Loopback:
|
||||||
@@ -109,8 +87,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
void thread_main(DWORD pid);
|
void thread_main(DWORD pid);
|
||||||
// Outcome of a hooked render session.
|
// Outcome of a hooked render session.
|
||||||
enum class HookedResult
|
enum class HookedResult {
|
||||||
{
|
|
||||||
Stopped, // clean stop (mirror stopping) -> done
|
Stopped, // clean stop (mirror stopping) -> done
|
||||||
Failed, // setup failed (format not renderable) -> caller falls back to loopback
|
Failed, // setup failed (format not renderable) -> caller falls back to loopback
|
||||||
Reinit, // the hook re-published the format (re-measure/override) -> re-read and retry
|
Reinit, // the hook re-published the format (re-measure/override) -> re-read and retry
|
||||||
@@ -140,8 +117,7 @@ private:
|
|||||||
|
|
||||||
// Operator ops queued by request_op (any thread) and applied to the rings on the
|
// Operator ops queued by request_op (any thread) and applied to the rings on the
|
||||||
// audio thread (which owns the mappings). Guarded by ops_mutex_.
|
// audio thread (which owns the mappings). Guarded by ops_mutex_.
|
||||||
struct PendingOp
|
struct PendingOp {
|
||||||
{
|
|
||||||
unsigned slot;
|
unsigned slot;
|
||||||
std::uint32_t kind, rate, channels, bits, format_tag;
|
std::uint32_t kind, rate, channels, bits, format_tag;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// WAVE_FORMAT_* values used here (kept local to avoid an mmreg.h dependency).
|
// WAVE_FORMAT_* values used here (kept local to avoid an mmreg.h dependency).
|
||||||
inline constexpr std::uint32_t kWaveFormatPcm = 1;
|
inline constexpr std::uint32_t kWaveFormatPcm = 1;
|
||||||
@@ -31,19 +30,14 @@ inline float soft_clip(float x)
|
|||||||
inline void mix_add(float* acc, const std::uint8_t* src, std::uint32_t samples, std::uint32_t format_tag,
|
inline void mix_add(float* acc, const std::uint8_t* src, std::uint32_t samples, std::uint32_t format_tag,
|
||||||
std::uint32_t bits)
|
std::uint32_t bits)
|
||||||
{
|
{
|
||||||
if (format_tag == kWaveFormatFloat && bits == 32)
|
if (format_tag == kWaveFormatFloat && bits == 32) {
|
||||||
{
|
|
||||||
const auto* f = reinterpret_cast<const float*>(src);
|
const auto* f = reinterpret_cast<const float*>(src);
|
||||||
for (std::uint32_t i = 0; i < samples; ++i)
|
for (std::uint32_t i = 0; i < samples; ++i) {
|
||||||
{
|
|
||||||
acc[i] += f[i];
|
acc[i] += f[i];
|
||||||
}
|
}
|
||||||
}
|
} else if (format_tag == kWaveFormatPcm && bits == 16) {
|
||||||
else if (format_tag == kWaveFormatPcm && bits == 16)
|
|
||||||
{
|
|
||||||
const auto* s = reinterpret_cast<const std::int16_t*>(src);
|
const auto* s = reinterpret_cast<const std::int16_t*>(src);
|
||||||
for (std::uint32_t i = 0; i < samples; ++i)
|
for (std::uint32_t i = 0; i < samples; ++i) {
|
||||||
{
|
|
||||||
acc[i] += static_cast<float>(s[i]) / 32768.0f;
|
acc[i] += static_cast<float>(s[i]) / 32768.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,19 +48,14 @@ inline void mix_add(float* acc, const std::uint8_t* src, std::uint32_t samples,
|
|||||||
inline void mix_store(std::uint8_t* dst, const float* acc, std::uint32_t samples, std::uint32_t format_tag,
|
inline void mix_store(std::uint8_t* dst, const float* acc, std::uint32_t samples, std::uint32_t format_tag,
|
||||||
std::uint32_t bits)
|
std::uint32_t bits)
|
||||||
{
|
{
|
||||||
if (format_tag == kWaveFormatFloat && bits == 32)
|
if (format_tag == kWaveFormatFloat && bits == 32) {
|
||||||
{
|
|
||||||
auto* f = reinterpret_cast<float*>(dst);
|
auto* f = reinterpret_cast<float*>(dst);
|
||||||
for (std::uint32_t i = 0; i < samples; ++i)
|
for (std::uint32_t i = 0; i < samples; ++i) {
|
||||||
{
|
|
||||||
f[i] = soft_clip(acc[i]);
|
f[i] = soft_clip(acc[i]);
|
||||||
}
|
}
|
||||||
}
|
} else if (format_tag == kWaveFormatPcm && bits == 16) {
|
||||||
else if (format_tag == kWaveFormatPcm && bits == 16)
|
|
||||||
{
|
|
||||||
auto* s = reinterpret_cast<std::int16_t*>(dst);
|
auto* s = reinterpret_cast<std::int16_t*>(dst);
|
||||||
for (std::uint32_t i = 0; i < samples; ++i)
|
for (std::uint32_t i = 0; i < samples; ++i) {
|
||||||
{
|
|
||||||
int v = static_cast<int>(soft_clip(acc[i]) * 32767.0f);
|
int v = static_cast<int>(soft_clip(acc[i]) * 32767.0f);
|
||||||
v = v > 32767 ? 32767 : (v < -32768 ? -32768 : v);
|
v = v > 32767 ? 32767 : (v < -32768 ? -32768 : v);
|
||||||
s[i] = static_cast<std::int16_t>(v);
|
s[i] = static_cast<std::int16_t>(v);
|
||||||
|
|||||||
@@ -11,14 +11,11 @@
|
|||||||
#include "coop/tool_paths.hpp"
|
#include "coop/tool_paths.hpp"
|
||||||
#include "util/utf8.hpp"
|
#include "util/utf8.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
namespace {
|
||||||
namespace
|
|
||||||
{
|
|
||||||
std::wstring to_lower(std::wstring s)
|
std::wstring to_lower(std::wstring s)
|
||||||
{
|
{
|
||||||
for (wchar_t& c : s)
|
for (wchar_t& c : s) {
|
||||||
{
|
|
||||||
c = static_cast<wchar_t>(::towlower(c));
|
c = static_cast<wchar_t>(::towlower(c));
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
@@ -29,8 +26,7 @@ std::wstring to_lower(std::wstring s)
|
|||||||
|
|
||||||
AudioOverrideStore::AudioOverrideStore(std::wstring path) : path_(std::move(path))
|
AudioOverrideStore::AudioOverrideStore(std::wstring path) : path_(std::move(path))
|
||||||
{
|
{
|
||||||
if (path_.empty())
|
if (path_.empty()) {
|
||||||
{
|
|
||||||
path_ = exe_directory() + L"coop_audio_overrides.ini";
|
path_ = exe_directory() + L"coop_audio_overrides.ini";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,46 +41,38 @@ void AudioOverrideStore::load()
|
|||||||
{
|
{
|
||||||
map_.clear();
|
map_.clear();
|
||||||
std::ifstream f(path_.c_str());
|
std::ifstream f(path_.c_str());
|
||||||
if (!f)
|
if (!f) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::string line;
|
std::string line;
|
||||||
while (std::getline(f, line))
|
while (std::getline(f, line)) {
|
||||||
{
|
|
||||||
// "<image> = <rate> <ch> <bits> <pcm|float>"; skip blank lines and # comments.
|
// "<image> = <rate> <ch> <bits> <pcm|float>"; skip blank lines and # comments.
|
||||||
const std::size_t hash = line.find('#');
|
const std::size_t hash = line.find('#');
|
||||||
if (hash != std::string::npos)
|
if (hash != std::string::npos) {
|
||||||
{
|
|
||||||
line.resize(hash);
|
line.resize(hash);
|
||||||
}
|
}
|
||||||
const std::size_t eq = line.find('=');
|
const std::size_t eq = line.find('=');
|
||||||
if (eq == std::string::npos)
|
if (eq == std::string::npos) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
std::string name = line.substr(0, eq);
|
std::string name = line.substr(0, eq);
|
||||||
// trim trailing/leading whitespace from the name
|
// trim trailing/leading whitespace from the name
|
||||||
while (!name.empty() && std::isspace(static_cast<unsigned char>(name.back())))
|
while (!name.empty() && std::isspace(static_cast<unsigned char>(name.back()))) {
|
||||||
{
|
|
||||||
name.pop_back();
|
name.pop_back();
|
||||||
}
|
}
|
||||||
std::size_t b = 0;
|
std::size_t b = 0;
|
||||||
while (b < name.size() && std::isspace(static_cast<unsigned char>(name[b])))
|
while (b < name.size() && std::isspace(static_cast<unsigned char>(name[b]))) {
|
||||||
{
|
|
||||||
++b;
|
++b;
|
||||||
}
|
}
|
||||||
name = name.substr(b);
|
name = name.substr(b);
|
||||||
if (name.empty())
|
if (name.empty()) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
std::istringstream vs(line.substr(eq + 1));
|
std::istringstream vs(line.substr(eq + 1));
|
||||||
AudioFormatOverride fmt;
|
AudioFormatOverride fmt;
|
||||||
std::string tag;
|
std::string tag;
|
||||||
vs >> fmt.rate >> fmt.channels >> fmt.bits >> tag;
|
vs >> fmt.rate >> fmt.channels >> fmt.bits >> tag;
|
||||||
if (!fmt.valid())
|
if (!fmt.valid()) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
fmt.format_tag = (tag == "float") ? WAVE_FORMAT_IEEE_FLOAT : WAVE_FORMAT_PCM;
|
fmt.format_tag = (tag == "float") ? WAVE_FORMAT_IEEE_FLOAT : WAVE_FORMAT_PCM;
|
||||||
@@ -95,8 +83,7 @@ void AudioOverrideStore::load()
|
|||||||
bool AudioOverrideStore::find(const std::wstring& image_name, AudioFormatOverride& out) const
|
bool AudioOverrideStore::find(const std::wstring& image_name, AudioFormatOverride& out) const
|
||||||
{
|
{
|
||||||
const auto it = map_.find(key_of(image_name));
|
const auto it = map_.find(key_of(image_name));
|
||||||
if (it == map_.end())
|
if (it == map_.end()) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
out = it->second;
|
out = it->second;
|
||||||
@@ -106,8 +93,7 @@ bool AudioOverrideStore::find(const std::wstring& image_name, AudioFormatOverrid
|
|||||||
void AudioOverrideStore::set(const std::wstring& image_name, const AudioFormatOverride& fmt, bool* differed)
|
void AudioOverrideStore::set(const std::wstring& image_name, const AudioFormatOverride& fmt, bool* differed)
|
||||||
{
|
{
|
||||||
const std::wstring key = key_of(image_name);
|
const std::wstring key = key_of(image_name);
|
||||||
if (differed != nullptr)
|
if (differed != nullptr) {
|
||||||
{
|
|
||||||
const auto it = map_.find(key);
|
const auto it = map_.find(key);
|
||||||
*differed = (it != map_.end() && it->second != fmt);
|
*differed = (it != map_.end() && it->second != fmt);
|
||||||
}
|
}
|
||||||
@@ -118,14 +104,12 @@ void AudioOverrideStore::set(const std::wstring& image_name, const AudioFormatOv
|
|||||||
void AudioOverrideStore::save() const
|
void AudioOverrideStore::save() const
|
||||||
{
|
{
|
||||||
std::ofstream f(path_.c_str(), std::ios::trunc);
|
std::ofstream f(path_.c_str(), std::ios::trunc);
|
||||||
if (!f)
|
if (!f) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
f << "# CoopAllTheThings per-game audio format overrides (auto-managed)\n";
|
f << "# CoopAllTheThings per-game audio format overrides (auto-managed)\n";
|
||||||
f << "# <image.exe> = <rate> <channels> <bits> <pcm|float>\n";
|
f << "# <image.exe> = <rate> <channels> <bits> <pcm|float>\n";
|
||||||
for (const auto& [name, fmt] : map_)
|
for (const auto& [name, fmt] : map_) {
|
||||||
{
|
|
||||||
f << narrow(name) << " = " << fmt.rate << ' ' << fmt.channels << ' ' << fmt.bits << ' '
|
f << narrow(name) << " = " << fmt.rate << ' ' << fmt.channels << ' ' << fmt.bits << ' '
|
||||||
<< (fmt.format_tag == WAVE_FORMAT_IEEE_FLOAT ? "float" : "pcm") << '\n';
|
<< (fmt.format_tag == WAVE_FORMAT_IEEE_FLOAT ? "float" : "pcm") << '\n';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,32 +13,23 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
struct AudioFormatOverride
|
struct AudioFormatOverride {
|
||||||
{
|
|
||||||
std::uint32_t rate = 0;
|
std::uint32_t rate = 0;
|
||||||
std::uint32_t channels = 0;
|
std::uint32_t channels = 0;
|
||||||
std::uint32_t bits = 0;
|
std::uint32_t bits = 0;
|
||||||
std::uint32_t format_tag = 0; // WAVE_FORMAT_PCM (1) / WAVE_FORMAT_IEEE_FLOAT (3)
|
std::uint32_t format_tag = 0; // WAVE_FORMAT_PCM (1) / WAVE_FORMAT_IEEE_FLOAT (3)
|
||||||
|
|
||||||
[[nodiscard]] bool valid() const
|
[[nodiscard]] bool valid() const { return rate != 0 && channels != 0 && bits != 0; }
|
||||||
{
|
|
||||||
return rate != 0 && channels != 0 && bits != 0;
|
|
||||||
}
|
|
||||||
bool operator==(const AudioFormatOverride& o) const
|
bool operator==(const AudioFormatOverride& o) const
|
||||||
{
|
{
|
||||||
return rate == o.rate && channels == o.channels && bits == o.bits && format_tag == o.format_tag;
|
return rate == o.rate && channels == o.channels && bits == o.bits && format_tag == o.format_tag;
|
||||||
}
|
}
|
||||||
bool operator!=(const AudioFormatOverride& o) const
|
bool operator!=(const AudioFormatOverride& o) const { return !(*this == o); }
|
||||||
{
|
|
||||||
return !(*this == o);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class AudioOverrideStore
|
class AudioOverrideStore {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
// `path` empty -> default (exe_dir/coop_audio_overrides.ini). Does not load yet.
|
// `path` empty -> default (exe_dir/coop_audio_overrides.ini). Does not load yet.
|
||||||
explicit AudioOverrideStore(std::wstring path = {});
|
explicit AudioOverrideStore(std::wstring path = {});
|
||||||
@@ -52,10 +43,7 @@ public:
|
|||||||
// existing entry for that game differed from `fmt` (caller warns the operator).
|
// existing entry for that game differed from `fmt` (caller warns the operator).
|
||||||
void set(const std::wstring& image_name, const AudioFormatOverride& fmt, bool* differed = nullptr);
|
void set(const std::wstring& image_name, const AudioFormatOverride& fmt, bool* differed = nullptr);
|
||||||
|
|
||||||
[[nodiscard]] const std::wstring& path() const
|
[[nodiscard]] const std::wstring& path() const { return path_; }
|
||||||
{
|
|
||||||
return path_;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static std::wstring key_of(const std::wstring& image_name); // lowercased basename
|
static std::wstring key_of(const std::wstring& image_name); // lowercased basename
|
||||||
|
|||||||
@@ -6,15 +6,12 @@
|
|||||||
#include <audioclientactivationparams.h>
|
#include <audioclientactivationparams.h>
|
||||||
#include <mmdeviceapi.h>
|
#include <mmdeviceapi.h>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
namespace {
|
||||||
namespace
|
|
||||||
{
|
|
||||||
|
|
||||||
// Completion handler for ActivateAudioInterfaceAsync. The call is async even when
|
// Completion handler for ActivateAudioInterfaceAsync. The call is async even when
|
||||||
// used synchronously: it signals `done`, and the caller waits on it.
|
// used synchronously: it signals `done`, and the caller waits on it.
|
||||||
class ActivateHandler : public IActivateAudioInterfaceCompletionHandler
|
class ActivateHandler : public IActivateAudioInterfaceCompletionHandler {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
HANDLE done = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
HANDLE done = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
||||||
HRESULT result = E_FAIL;
|
HRESULT result = E_FAIL;
|
||||||
@@ -25,16 +22,13 @@ public:
|
|||||||
HRESULT activate_hr = E_FAIL;
|
HRESULT activate_hr = E_FAIL;
|
||||||
IUnknown* punk = nullptr;
|
IUnknown* punk = nullptr;
|
||||||
HRESULT hr = op->GetActivateResult(&activate_hr, &punk);
|
HRESULT hr = op->GetActivateResult(&activate_hr, &punk);
|
||||||
if (SUCCEEDED(hr))
|
if (SUCCEEDED(hr)) {
|
||||||
{
|
|
||||||
hr = activate_hr;
|
hr = activate_hr;
|
||||||
}
|
}
|
||||||
if (SUCCEEDED(hr) && punk)
|
if (SUCCEEDED(hr) && punk) {
|
||||||
{
|
|
||||||
hr = punk->QueryInterface(__uuidof(IAudioClient), reinterpret_cast<void**>(&client));
|
hr = punk->QueryInterface(__uuidof(IAudioClient), reinterpret_cast<void**>(&client));
|
||||||
}
|
}
|
||||||
if (punk)
|
if (punk) {
|
||||||
{
|
|
||||||
punk->Release();
|
punk->Release();
|
||||||
}
|
}
|
||||||
result = hr;
|
result = hr;
|
||||||
@@ -44,16 +38,14 @@ public:
|
|||||||
|
|
||||||
STDMETHODIMP QueryInterface(REFIID riid, void** ppv) override
|
STDMETHODIMP QueryInterface(REFIID riid, void** ppv) override
|
||||||
{
|
{
|
||||||
if (riid == __uuidof(IUnknown) || riid == __uuidof(IActivateAudioInterfaceCompletionHandler))
|
if (riid == __uuidof(IUnknown) || riid == __uuidof(IActivateAudioInterfaceCompletionHandler)) {
|
||||||
{
|
|
||||||
*ppv = static_cast<IActivateAudioInterfaceCompletionHandler*>(this);
|
*ppv = static_cast<IActivateAudioInterfaceCompletionHandler*>(this);
|
||||||
AddRef();
|
AddRef();
|
||||||
return S_OK;
|
return S_OK;
|
||||||
}
|
}
|
||||||
// Mark the handler agile; ActivateAudioInterfaceAsync requires an agile
|
// Mark the handler agile; ActivateAudioInterfaceAsync requires an agile
|
||||||
// completion handler and otherwise rejects the call (E_ILLEGAL_METHOD_CALL).
|
// completion handler and otherwise rejects the call (E_ILLEGAL_METHOD_CALL).
|
||||||
if (riid == __uuidof(IAgileObject))
|
if (riid == __uuidof(IAgileObject)) {
|
||||||
{
|
|
||||||
*ppv = static_cast<IUnknown*>(this);
|
*ppv = static_cast<IUnknown*>(this);
|
||||||
AddRef();
|
AddRef();
|
||||||
return S_OK;
|
return S_OK;
|
||||||
@@ -61,15 +53,11 @@ public:
|
|||||||
*ppv = nullptr;
|
*ppv = nullptr;
|
||||||
return E_NOINTERFACE;
|
return E_NOINTERFACE;
|
||||||
}
|
}
|
||||||
STDMETHODIMP_(ULONG) AddRef() override
|
STDMETHODIMP_(ULONG) AddRef() override { return ++ref_; }
|
||||||
{
|
|
||||||
return ++ref_;
|
|
||||||
}
|
|
||||||
STDMETHODIMP_(ULONG) Release() override
|
STDMETHODIMP_(ULONG) Release() override
|
||||||
{
|
{
|
||||||
const ULONG r = --ref_;
|
const ULONG r = --ref_;
|
||||||
if (r == 0)
|
if (r == 0) {
|
||||||
{
|
|
||||||
delete this;
|
delete this;
|
||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
@@ -78,8 +66,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
~ActivateHandler()
|
~ActivateHandler()
|
||||||
{
|
{
|
||||||
if (done)
|
if (done) {
|
||||||
{
|
|
||||||
CloseHandle(done);
|
CloseHandle(done);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,26 +87,20 @@ HRESULT activate_loopback_client(DWORD pid, IAudioClient** out)
|
|||||||
|
|
||||||
auto* handler = new ActivateHandler();
|
auto* handler = new ActivateHandler();
|
||||||
HRESULT hr = E_FAIL;
|
HRESULT hr = E_FAIL;
|
||||||
if (handler->done)
|
if (handler->done) {
|
||||||
{
|
|
||||||
IActivateAudioInterfaceAsyncOperation* op = nullptr;
|
IActivateAudioInterfaceAsyncOperation* op = nullptr;
|
||||||
hr = ActivateAudioInterfaceAsync(VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK, __uuidof(IAudioClient),
|
hr = ActivateAudioInterfaceAsync(VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK, __uuidof(IAudioClient), &pv, handler,
|
||||||
&pv, handler, &op);
|
&op);
|
||||||
if (SUCCEEDED(hr))
|
if (SUCCEEDED(hr)) {
|
||||||
{
|
|
||||||
WaitForSingleObject(handler->done, INFINITE);
|
WaitForSingleObject(handler->done, INFINITE);
|
||||||
hr = handler->result;
|
hr = handler->result;
|
||||||
if (SUCCEEDED(hr))
|
if (SUCCEEDED(hr)) {
|
||||||
{
|
|
||||||
*out = handler->client; // transfer the QueryInterface reference
|
*out = handler->client; // transfer the QueryInterface reference
|
||||||
}
|
} else if (handler->client) {
|
||||||
else if (handler->client)
|
|
||||||
{
|
|
||||||
handler->client->Release();
|
handler->client->Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (op)
|
if (op) {
|
||||||
{
|
|
||||||
op->Release();
|
op->Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,19 +113,16 @@ HRESULT activate_loopback_client(DWORD pid, IAudioClient** out)
|
|||||||
WAVEFORMATEX* default_render_format()
|
WAVEFORMATEX* default_render_format()
|
||||||
{
|
{
|
||||||
IMMDeviceEnumerator* enumerator = nullptr;
|
IMMDeviceEnumerator* enumerator = nullptr;
|
||||||
if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
|
if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator),
|
||||||
__uuidof(IMMDeviceEnumerator), reinterpret_cast<void**>(&enumerator))))
|
reinterpret_cast<void**>(&enumerator)))) {
|
||||||
{
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
IMMDevice* endpoint = nullptr;
|
IMMDevice* endpoint = nullptr;
|
||||||
WAVEFORMATEX* fmt = nullptr;
|
WAVEFORMATEX* fmt = nullptr;
|
||||||
if (SUCCEEDED(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint)))
|
if (SUCCEEDED(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint))) {
|
||||||
{
|
|
||||||
IAudioClient* client = nullptr;
|
IAudioClient* client = nullptr;
|
||||||
if (SUCCEEDED(endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr,
|
if (SUCCEEDED(
|
||||||
reinterpret_cast<void**>(&client))))
|
endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast<void**>(&client)))) {
|
||||||
{
|
|
||||||
client->GetMixFormat(&fmt);
|
client->GetMixFormat(&fmt);
|
||||||
client->Release();
|
client->Release();
|
||||||
}
|
}
|
||||||
@@ -174,14 +152,12 @@ void ProcessLoopbackCapture::set_status(std::string s)
|
|||||||
bool ProcessLoopbackCapture::start(DWORD pid, const WAVEFORMATEX* format, FrameSink sink)
|
bool ProcessLoopbackCapture::start(DWORD pid, const WAVEFORMATEX* format, FrameSink sink)
|
||||||
{
|
{
|
||||||
stop();
|
stop();
|
||||||
if (!pid || !format)
|
if (!pid || !format) {
|
||||||
{
|
|
||||||
set_status("No target/format.");
|
set_status("No target/format.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
stop_event_ = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
stop_event_ = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||||
if (!stop_event_)
|
if (!stop_event_) {
|
||||||
{
|
|
||||||
set_status("CreateEvent failed.");
|
set_status("CreateEvent failed.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -192,23 +168,19 @@ bool ProcessLoopbackCapture::start(DWORD pid, const WAVEFORMATEX* format, FrameS
|
|||||||
std::memcpy(fmt_copy.data(), format, fmt_copy.size());
|
std::memcpy(fmt_copy.data(), format, fmt_copy.size());
|
||||||
|
|
||||||
set_status("Starting…");
|
set_status("Starting…");
|
||||||
thread_ = std::thread(&ProcessLoopbackCapture::thread_main, this, pid, std::move(fmt_copy),
|
thread_ = std::thread(&ProcessLoopbackCapture::thread_main, this, pid, std::move(fmt_copy), std::move(sink));
|
||||||
std::move(sink));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProcessLoopbackCapture::stop()
|
void ProcessLoopbackCapture::stop()
|
||||||
{
|
{
|
||||||
if (stop_event_)
|
if (stop_event_) {
|
||||||
{
|
|
||||||
SetEvent(stop_event_);
|
SetEvent(stop_event_);
|
||||||
}
|
}
|
||||||
if (thread_.joinable())
|
if (thread_.joinable()) {
|
||||||
{
|
|
||||||
thread_.join();
|
thread_.join();
|
||||||
}
|
}
|
||||||
if (stop_event_)
|
if (stop_event_) {
|
||||||
{
|
|
||||||
CloseHandle(stop_event_);
|
CloseHandle(stop_event_);
|
||||||
stop_event_ = nullptr;
|
stop_event_ = nullptr;
|
||||||
}
|
}
|
||||||
@@ -231,18 +203,15 @@ void ProcessLoopbackCapture::thread_main(DWORD pid, std::vector<BYTE> format, Fr
|
|||||||
set_status(buf);
|
set_status(buf);
|
||||||
};
|
};
|
||||||
|
|
||||||
do
|
do {
|
||||||
{
|
|
||||||
HRESULT hr = activate_loopback_client(pid, &client);
|
HRESULT hr = activate_loopback_client(pid, &client);
|
||||||
if (FAILED(hr))
|
if (FAILED(hr)) {
|
||||||
{
|
|
||||||
fail("Process loopback activate", hr);
|
fail("Process loopback activate", hr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
capture_event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
capture_event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
||||||
if (!capture_event)
|
if (!capture_event) {
|
||||||
{
|
|
||||||
fail("CreateEvent(capture)", HRESULT_FROM_WIN32(GetLastError()));
|
fail("CreateEvent(capture)", HRESULT_FROM_WIN32(GetLastError()));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -250,27 +219,22 @@ void ProcessLoopbackCapture::thread_main(DWORD pid, std::vector<BYTE> format, Fr
|
|||||||
// Process loopback requires shared mode, the LOOPBACK + EVENTCALLBACK flags,
|
// Process loopback requires shared mode, the LOOPBACK + EVENTCALLBACK flags,
|
||||||
// and zero buffer/periodicity (there is no device period to query).
|
// and zero buffer/periodicity (there is no device period to query).
|
||||||
hr = client->Initialize(AUDCLNT_SHAREMODE_SHARED,
|
hr = client->Initialize(AUDCLNT_SHAREMODE_SHARED,
|
||||||
AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK, 0, 0,
|
AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK, 0, 0, fmt, nullptr);
|
||||||
fmt, nullptr);
|
if (FAILED(hr)) {
|
||||||
if (FAILED(hr))
|
|
||||||
{
|
|
||||||
fail("Capture Initialize", hr);
|
fail("Capture Initialize", hr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
hr = client->SetEventHandle(capture_event);
|
hr = client->SetEventHandle(capture_event);
|
||||||
if (FAILED(hr))
|
if (FAILED(hr)) {
|
||||||
{
|
|
||||||
fail("Capture SetEventHandle", hr);
|
fail("Capture SetEventHandle", hr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
hr = client->GetService(__uuidof(IAudioCaptureClient), reinterpret_cast<void**>(&capture));
|
hr = client->GetService(__uuidof(IAudioCaptureClient), reinterpret_cast<void**>(&capture));
|
||||||
if (FAILED(hr))
|
if (FAILED(hr)) {
|
||||||
{
|
|
||||||
fail("GetService(CaptureClient)", hr);
|
fail("GetService(CaptureClient)", hr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (FAILED(hr = client->Start()))
|
if (FAILED(hr = client->Start())) {
|
||||||
{
|
|
||||||
fail("Capture Start", hr);
|
fail("Capture Start", hr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -279,47 +243,38 @@ void ProcessLoopbackCapture::thread_main(DWORD pid, std::vector<BYTE> format, Fr
|
|||||||
running_.store(true, std::memory_order_release);
|
running_.store(true, std::memory_order_release);
|
||||||
|
|
||||||
HANDLE waits[2] = {stop_event_, capture_event};
|
HANDLE waits[2] = {stop_event_, capture_event};
|
||||||
for (;;)
|
for (;;) {
|
||||||
{
|
|
||||||
const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200);
|
const DWORD w = WaitForMultipleObjects(2, waits, FALSE, 200);
|
||||||
if (w == WAIT_OBJECT_0)
|
if (w == WAIT_OBJECT_0) {
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
UINT32 packet = 0;
|
UINT32 packet = 0;
|
||||||
while (SUCCEEDED(capture->GetNextPacketSize(&packet)) && packet > 0)
|
while (SUCCEEDED(capture->GetNextPacketSize(&packet)) && packet > 0) {
|
||||||
{
|
|
||||||
BYTE* data = nullptr;
|
BYTE* data = nullptr;
|
||||||
UINT32 frames = 0;
|
UINT32 frames = 0;
|
||||||
DWORD flags = 0;
|
DWORD flags = 0;
|
||||||
if (FAILED(capture->GetBuffer(&data, &frames, &flags, nullptr, nullptr)))
|
if (FAILED(capture->GetBuffer(&data, &frames, &flags, nullptr, nullptr))) {
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const bool silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0;
|
const bool silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0;
|
||||||
frames_captured_.fetch_add(frames, std::memory_order_relaxed);
|
frames_captured_.fetch_add(frames, std::memory_order_relaxed);
|
||||||
if (!silent && frames > 0)
|
if (!silent && frames > 0) {
|
||||||
{
|
|
||||||
// Count frames that carry any non-zero sample.
|
// Count frames that carry any non-zero sample.
|
||||||
const BYTE* p = data;
|
const BYTE* p = data;
|
||||||
const BYTE* end = data + static_cast<size_t>(frames) * frame_bytes;
|
const BYTE* end = data + static_cast<size_t>(frames) * frame_bytes;
|
||||||
bool any = false;
|
bool any = false;
|
||||||
for (; p < end; ++p)
|
for (; p < end; ++p) {
|
||||||
{
|
if (*p != 0) {
|
||||||
if (*p != 0)
|
|
||||||
{
|
|
||||||
any = true;
|
any = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (any)
|
if (any) {
|
||||||
{
|
|
||||||
nonsilent_frames_.fetch_add(frames, std::memory_order_relaxed);
|
nonsilent_frames_.fetch_add(frames, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sink)
|
if (sink) {
|
||||||
{
|
|
||||||
sink(data, frames, silent);
|
sink(data, frames, silent);
|
||||||
}
|
}
|
||||||
capture->ReleaseBuffer(frames);
|
capture->ReleaseBuffer(frames);
|
||||||
@@ -329,26 +284,21 @@ void ProcessLoopbackCapture::thread_main(DWORD pid, std::vector<BYTE> format, Fr
|
|||||||
client->Stop();
|
client->Stop();
|
||||||
} while (false);
|
} while (false);
|
||||||
|
|
||||||
if (running_.load(std::memory_order_acquire))
|
if (running_.load(std::memory_order_acquire)) {
|
||||||
{
|
|
||||||
running_.store(false, std::memory_order_release);
|
running_.store(false, std::memory_order_release);
|
||||||
set_status("Stopped.");
|
set_status("Stopped.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (capture)
|
if (capture) {
|
||||||
{
|
|
||||||
capture->Release();
|
capture->Release();
|
||||||
}
|
}
|
||||||
if (client)
|
if (client) {
|
||||||
{
|
|
||||||
client->Release();
|
client->Release();
|
||||||
}
|
}
|
||||||
if (capture_event)
|
if (capture_event) {
|
||||||
{
|
|
||||||
CloseHandle(capture_event);
|
CloseHandle(capture_event);
|
||||||
}
|
}
|
||||||
if (com_ok)
|
if (com_ok) {
|
||||||
{
|
|
||||||
CoUninitialize();
|
CoUninitialize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,13 @@
|
|||||||
|
|
||||||
#include <mmreg.h> // WAVEFORMATEX
|
#include <mmreg.h> // WAVEFORMATEX
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// Default render endpoint mix format (caller owns the returned pointer; free with
|
// Default render endpoint mix format (caller owns the returned pointer; free with
|
||||||
// CoTaskMemFree). Returns nullptr on failure. Requires a COM-initialized thread.
|
// CoTaskMemFree). Returns nullptr on failure. Requires a COM-initialized thread.
|
||||||
WAVEFORMATEX* default_render_format();
|
WAVEFORMATEX* default_render_format();
|
||||||
|
|
||||||
class ProcessLoopbackCapture
|
class ProcessLoopbackCapture {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
// Called on the capture thread for each delivered packet. `silent` means the
|
// Called on the capture thread for each delivered packet. `silent` means the
|
||||||
// engine flagged the packet as silence (data may be undefined).
|
// engine flagged the packet as silence (data may be undefined).
|
||||||
@@ -41,20 +39,11 @@ public:
|
|||||||
bool start(DWORD pid, const WAVEFORMATEX* format, FrameSink sink);
|
bool start(DWORD pid, const WAVEFORMATEX* format, FrameSink sink);
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
[[nodiscard]] bool running() const
|
[[nodiscard]] bool running() const { return running_.load(std::memory_order_acquire); }
|
||||||
{
|
|
||||||
return running_.load(std::memory_order_acquire);
|
|
||||||
}
|
|
||||||
[[nodiscard]] std::string status() const;
|
[[nodiscard]] std::string status() const;
|
||||||
|
|
||||||
[[nodiscard]] std::uint64_t frames_captured() const
|
[[nodiscard]] std::uint64_t frames_captured() const { return frames_captured_.load(std::memory_order_relaxed); }
|
||||||
{
|
[[nodiscard]] std::uint64_t nonsilent_frames() const { return nonsilent_frames_.load(std::memory_order_relaxed); }
|
||||||
return frames_captured_.load(std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
[[nodiscard]] std::uint64_t nonsilent_frames() const
|
|
||||||
{
|
|
||||||
return nonsilent_frames_.load(std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void thread_main(DWORD pid, std::vector<BYTE> format, FrameSink sink);
|
void thread_main(DWORD pid, std::vector<BYTE> format, FrameSink sink);
|
||||||
|
|||||||
@@ -22,11 +22,9 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
struct RenderPacer
|
struct RenderPacer {
|
||||||
{
|
|
||||||
std::uint32_t prime_frames = 0; // cushion to (re)build before playback resumes
|
std::uint32_t prime_frames = 0; // cushion to (re)build before playback resumes
|
||||||
bool primed = false;
|
bool primed = false;
|
||||||
|
|
||||||
@@ -37,28 +35,22 @@ struct RenderPacer
|
|||||||
// Returns the frame count to write (0 while still priming or when the ring is empty).
|
// Returns the frame count to write (0 while still priming or when the ring is empty).
|
||||||
std::uint32_t pump(std::uint32_t avail, std::uint32_t have, std::uint32_t padding)
|
std::uint32_t pump(std::uint32_t avail, std::uint32_t have, std::uint32_t padding)
|
||||||
{
|
{
|
||||||
if (!primed && have >= prime_frames)
|
if (!primed && have >= prime_frames) {
|
||||||
{
|
|
||||||
primed = true;
|
primed = true;
|
||||||
}
|
}
|
||||||
if (!primed)
|
if (!primed) {
|
||||||
{
|
|
||||||
return 0; // still building the initial / post-starvation cushion
|
return 0; // still building the initial / post-starvation cushion
|
||||||
}
|
}
|
||||||
const std::uint32_t to_write = std::min(avail, have);
|
const std::uint32_t to_write = std::min(avail, have);
|
||||||
// Genuine starvation only: the device emptied and the ring has nothing to give.
|
// Genuine starvation only: the device emptied and the ring has nothing to give.
|
||||||
// A partial fill (have < avail) is normal jitter and must NOT trigger a re-prime.
|
// A partial fill (have < avail) is normal jitter and must NOT trigger a re-prime.
|
||||||
if (padding == 0 && have == 0)
|
if (padding == 0 && have == 0) {
|
||||||
{
|
|
||||||
primed = false;
|
primed = false;
|
||||||
}
|
}
|
||||||
return to_write;
|
return to_write;
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset()
|
void reset() { primed = false; }
|
||||||
{
|
|
||||||
primed = false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace coop
|
} // namespace coop
|
||||||
|
|||||||
@@ -10,11 +10,9 @@
|
|||||||
#include "ui/app_chrome.hpp"
|
#include "ui/app_chrome.hpp"
|
||||||
#include "util/utf8.hpp"
|
#include "util/utf8.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f);
|
const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f);
|
||||||
const ImVec4 kAmber(1.0f, 0.8f, 0.3f, 1.0f);
|
const ImVec4 kAmber(1.0f, 0.8f, 0.3f, 1.0f);
|
||||||
@@ -22,8 +20,7 @@ const ImVec4 kRed(1.0f, 0.45f, 0.4f, 1.0f);
|
|||||||
|
|
||||||
const char* format_tag_name(std::uint32_t tag)
|
const char* format_tag_name(std::uint32_t tag)
|
||||||
{
|
{
|
||||||
switch (tag)
|
switch (tag) {
|
||||||
{
|
|
||||||
case WAVE_FORMAT_PCM:
|
case WAVE_FORMAT_PCM:
|
||||||
return "PCM";
|
return "PCM";
|
||||||
case WAVE_FORMAT_IEEE_FLOAT:
|
case WAVE_FORMAT_IEEE_FLOAT:
|
||||||
@@ -38,8 +35,7 @@ const char* format_tag_name(std::uint32_t tag)
|
|||||||
// How the hooked backend learned a stream's format (drives the pitch correctness).
|
// How the hooked backend learned a stream's format (drives the pitch correctness).
|
||||||
const char* audio_format_state_name(std::uint32_t state)
|
const char* audio_format_state_name(std::uint32_t state)
|
||||||
{
|
{
|
||||||
switch (state)
|
switch (state) {
|
||||||
{
|
|
||||||
case AudioFormat_Exact:
|
case AudioFormat_Exact:
|
||||||
return "known (from game)";
|
return "known (from game)";
|
||||||
case AudioFormat_Measuring:
|
case AudioFormat_Measuring:
|
||||||
@@ -57,8 +53,7 @@ const char* audio_format_state_name(std::uint32_t state)
|
|||||||
|
|
||||||
ImVec4 audio_format_state_color(std::uint32_t state)
|
ImVec4 audio_format_state_color(std::uint32_t state)
|
||||||
{
|
{
|
||||||
switch (state)
|
switch (state) {
|
||||||
{
|
|
||||||
case AudioFormat_Exact:
|
case AudioFormat_Exact:
|
||||||
case AudioFormat_Measured:
|
case AudioFormat_Measured:
|
||||||
case AudioFormat_Override:
|
case AudioFormat_Override:
|
||||||
@@ -81,20 +76,17 @@ std::string image_basename(const std::wstring& image_path)
|
|||||||
|
|
||||||
std::wstring AudioPanel::image_name_from_pid(DWORD pid)
|
std::wstring AudioPanel::image_name_from_pid(DWORD pid)
|
||||||
{
|
{
|
||||||
if (pid == 0)
|
if (pid == 0) {
|
||||||
{
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
||||||
if (h == nullptr)
|
if (h == nullptr) {
|
||||||
{
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
wchar_t buf[MAX_PATH] = {};
|
wchar_t buf[MAX_PATH] = {};
|
||||||
DWORD n = MAX_PATH;
|
DWORD n = MAX_PATH;
|
||||||
std::wstring name;
|
std::wstring name;
|
||||||
if (QueryFullProcessImageNameW(h, 0, buf, &n))
|
if (QueryFullProcessImageNameW(h, 0, buf, &n)) {
|
||||||
{
|
|
||||||
name.assign(buf, n);
|
name.assign(buf, n);
|
||||||
}
|
}
|
||||||
CloseHandle(h);
|
CloseHandle(h);
|
||||||
@@ -110,24 +102,20 @@ void AudioPanel::manage_overrides(const HookStatusView& status, DWORD pid)
|
|||||||
override_applied_ = false;
|
override_applied_ = false;
|
||||||
exact_saved_ = false;
|
exact_saved_ = false;
|
||||||
}
|
}
|
||||||
if (pid == 0 || target_image_.empty() || !mirror_.running() || status.audio_streams_seen == 0)
|
if (pid == 0 || target_image_.empty() || !mirror_.running() || status.audio_streams_seen == 0) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AudioStreamInfo& s = status.audio_streams[0]; // the primary (mirrored) stream
|
const AudioStreamInfo& s = status.audio_streams[0]; // the primary (mirrored) stream
|
||||||
const std::uint32_t st = s.format_state;
|
const std::uint32_t st = s.format_state;
|
||||||
if (st == AudioFormat_Exact)
|
if (st == AudioFormat_Exact) {
|
||||||
{
|
|
||||||
// Ground truth: persist it as this game's override (so a later late-attach is fixed).
|
// Ground truth: persist it as this game's override (so a later late-attach is fixed).
|
||||||
if (!exact_saved_)
|
if (!exact_saved_) {
|
||||||
{
|
|
||||||
exact_saved_ = true;
|
exact_saved_ = true;
|
||||||
const AudioFormatOverride fmt{s.sample_rate, s.channels, s.bits, s.format_tag};
|
const AudioFormatOverride fmt{s.sample_rate, s.channels, s.bits, s.format_tag};
|
||||||
bool differed = false;
|
bool differed = false;
|
||||||
overrides_.set(target_image_, fmt, &differed);
|
overrides_.set(target_image_, fmt, &differed);
|
||||||
if (differed && logger_)
|
if (differed && logger_) {
|
||||||
{
|
|
||||||
char msg[160];
|
char msg[160];
|
||||||
std::snprintf(msg, sizeof(msg),
|
std::snprintf(msg, sizeof(msg),
|
||||||
"%s: exact format %uHz/%uch/%ubit caught -> replaced a DIFFERING saved override",
|
"%s: exact format %uHz/%uch/%ubit caught -> replaced a DIFFERING saved override",
|
||||||
@@ -135,19 +123,14 @@ void AudioPanel::manage_overrides(const HookStatusView& status, DWORD pid)
|
|||||||
logger_(LogLevel_Warn, msg);
|
logger_(LogLevel_Warn, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if (st == AudioFormat_Measuring || st == AudioFormat_Measured || st == AudioFormat_LowConfidence) {
|
||||||
else if (st == AudioFormat_Measuring || st == AudioFormat_Measured || st == AudioFormat_LowConfidence)
|
|
||||||
{
|
|
||||||
// A guessed stream: if we have a saved override for this game, apply it.
|
// A guessed stream: if we have a saved override for this game, apply it.
|
||||||
if (!override_applied_)
|
if (!override_applied_) {
|
||||||
{
|
|
||||||
override_applied_ = true;
|
override_applied_ = true;
|
||||||
AudioFormatOverride ov;
|
AudioFormatOverride ov;
|
||||||
if (overrides_.find(target_image_, ov))
|
if (overrides_.find(target_image_, ov)) {
|
||||||
{
|
|
||||||
mirror_.request_op(0, AudioRingOp_Override, ov.rate, ov.channels, ov.bits, ov.format_tag);
|
mirror_.request_op(0, AudioRingOp_Override, ov.rate, ov.channels, ov.bits, ov.format_tag);
|
||||||
if (logger_)
|
if (logger_) {
|
||||||
{
|
|
||||||
char msg[160];
|
char msg[160];
|
||||||
std::snprintf(msg, sizeof(msg), "%s: applied saved audio override %uHz/%uch/%ubit",
|
std::snprintf(msg, sizeof(msg), "%s: applied saved audio override %uHz/%uch/%ubit",
|
||||||
image_basename(target_image_).c_str(), ov.rate, ov.channels, ov.bits);
|
image_basename(target_image_).c_str(), ov.rate, ov.channels, ov.bits);
|
||||||
@@ -161,12 +144,10 @@ void AudioPanel::manage_overrides(const HookStatusView& status, DWORD pid)
|
|||||||
void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
|
void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
|
||||||
{
|
{
|
||||||
DWORD pid = 0;
|
DWORD pid = 0;
|
||||||
if (target_ != nullptr && IsWindow(target_))
|
if (target_ != nullptr && IsWindow(target_)) {
|
||||||
{
|
|
||||||
GetWindowThreadProcessId(target_, &pid);
|
GetWindowThreadProcessId(target_, &pid);
|
||||||
}
|
}
|
||||||
if (dev_pid_ != 0)
|
if (dev_pid_ != 0) {
|
||||||
{
|
|
||||||
pid = dev_pid_; // test harness: a windowless target (e.g. coop_tone) has no HWND
|
pid = dev_pid_; // test harness: a windowless target (e.g. coop_tone) has no HWND
|
||||||
}
|
}
|
||||||
const bool have_target = pid != 0;
|
const bool have_target = pid != 0;
|
||||||
@@ -175,26 +156,20 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
|
|||||||
ImGui::Begin("Audio mirror");
|
ImGui::Begin("Audio mirror");
|
||||||
|
|
||||||
ImGui::BeginDisabled(!have_target);
|
ImGui::BeginDisabled(!have_target);
|
||||||
if (ImGui::Checkbox("Mirror game audio", &enabled_))
|
if (ImGui::Checkbox("Mirror game audio", &enabled_)) {
|
||||||
{
|
if (!enabled_) {
|
||||||
if (!enabled_)
|
|
||||||
{
|
|
||||||
mirror_.stop();
|
mirror_.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
if (!have_target)
|
if (!have_target) {
|
||||||
{
|
|
||||||
ImGui::TextDisabled("Inject into a game first (its audio is the source).");
|
ImGui::TextDisabled("Inject into a game first (its audio is the source).");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start when enabled and the target process changes; stop if it disappears.
|
// Start when enabled and the target process changes; stop if it disappears.
|
||||||
if (enabled_ && pid != 0 && mirror_.target_pid() != pid)
|
if (enabled_ && pid != 0 && mirror_.target_pid() != pid) {
|
||||||
{
|
|
||||||
mirror_.start(pid);
|
mirror_.start(pid);
|
||||||
}
|
} else if (enabled_ && pid == 0 && mirror_.target_pid() != 0) {
|
||||||
else if (enabled_ && pid == 0 && mirror_.target_pid() != 0)
|
|
||||||
{
|
|
||||||
mirror_.stop();
|
mirror_.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,14 +189,12 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
|
|||||||
demo_ ? std::string("render-hook did not publish a format in time; using WASAPI process loopback.")
|
demo_ ? std::string("render-hook did not publish a format in time; using WASAPI process loopback.")
|
||||||
: mirror_.fallback_reason();
|
: mirror_.fallback_reason();
|
||||||
|
|
||||||
if (running)
|
if (running) {
|
||||||
{
|
|
||||||
const bool hooked = src == AudioMirror::Source::Hooked;
|
const bool hooked = src == AudioMirror::Source::Hooked;
|
||||||
ImGui::TextColored(kGreen, "Mirroring %u Hz, %u ch", m_rate, m_ch);
|
ImGui::TextColored(kGreen, "Mirroring %u Hz, %u ch", m_rate, m_ch);
|
||||||
ImGui::Text("Source:");
|
ImGui::Text("Source:");
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (hooked)
|
if (hooked) {
|
||||||
{
|
|
||||||
// The hooked path only silences (no echo) an EXACT / override format, whose frame
|
// The hooked path only silences (no echo) an EXACT / override format, whose frame
|
||||||
// size is known. A guessed stream is captured but not silenced (silencing a guessed
|
// size is known. A guessed stream is captured but not silenced (silencing a guessed
|
||||||
// buffer could over-write it), so the game stays audible -- an echo. Make that clear.
|
// buffer could over-write it), so the game stays audible -- an echo. Make that clear.
|
||||||
@@ -229,9 +202,7 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
|
|||||||
const bool no_echo = (st == AudioFormat_Exact || st == AudioFormat_Override);
|
const bool no_echo = (st == AudioFormat_Exact || st == AudioFormat_Override);
|
||||||
ImGui::TextColored(no_echo ? kGreen : kAmber, "Hooked (%s)",
|
ImGui::TextColored(no_echo ? kGreen : kAmber, "Hooked (%s)",
|
||||||
no_echo ? "no echo" : "echo -- guessed format");
|
no_echo ? "no echo" : "echo -- guessed format");
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextColored(kAmber, "%s", demo_ ? "Loopback (echo)" : mirror_.source_name());
|
ImGui::TextColored(kAmber, "%s", demo_ ? "Loopback (echo)" : mirror_.source_name());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,26 +211,21 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
|
|||||||
// captured post-mix at the device endpoint format, so it's always known-correct.
|
// captured post-mix at the device endpoint format, so it's always known-correct.
|
||||||
ImGui::Text("Format:");
|
ImGui::Text("Format:");
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (hooked)
|
if (hooked) {
|
||||||
{
|
|
||||||
const std::uint32_t st = status.audio_streams[0].format_state; // [0] is the primary
|
const std::uint32_t st = status.audio_streams[0].format_state; // [0] is the primary
|
||||||
ImGui::TextColored(audio_format_state_color(st), "%s", audio_format_state_name(st));
|
ImGui::TextColored(audio_format_state_color(st), "%s", audio_format_state_name(st));
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextColored(kGreen, "device endpoint (known, post-mix)");
|
ImGui::TextColored(kGreen, "device endpoint (known, post-mix)");
|
||||||
}
|
}
|
||||||
ImGui::Text("Buffered: %4u ms", m_buffered);
|
ImGui::Text("Buffered: %4u ms", m_buffered);
|
||||||
}
|
}
|
||||||
if (!mirror_status.empty())
|
if (!mirror_status.empty()) {
|
||||||
{
|
|
||||||
ImGui::TextWrapped("%s", mirror_status.c_str());
|
ImGui::TextWrapped("%s", mirror_status.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Why we're on loopback instead of the no-echo hooked path (empty when hooked). Amber
|
// Why we're on loopback instead of the no-echo hooked path (empty when hooked). Amber
|
||||||
// because it's a degraded-but-working state that auto-resolves when the hook catches up.
|
// because it's a degraded-but-working state that auto-resolves when the hook catches up.
|
||||||
if (!reason.empty())
|
if (!reason.empty()) {
|
||||||
{
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, kAmber);
|
ImGui::PushStyleColor(ImGuiCol_Text, kAmber);
|
||||||
ImGui::TextWrapped("Why loopback: %s", reason.c_str());
|
ImGui::TextWrapped("Why loopback: %s", reason.c_str());
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
@@ -267,25 +233,18 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
|
|||||||
|
|
||||||
// Only the loopback path leaves the game audible locally (the echo); the
|
// Only the loopback path leaves the game audible locally (the echo); the
|
||||||
// hooked path silences it, so don't warn there.
|
// hooked path silences it, so don't warn there.
|
||||||
if (src == AudioMirror::Source::Loopback)
|
if (src == AudioMirror::Source::Loopback) {
|
||||||
{
|
|
||||||
bool audio_hook_on = false;
|
bool audio_hook_on = false;
|
||||||
const std::uint32_t hn =
|
const std::uint32_t hn = status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries;
|
||||||
status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries;
|
for (std::uint32_t i = 0; i < hn; ++i) {
|
||||||
for (std::uint32_t i = 0; i < hn; ++i)
|
if (status.hook_entries[i].subsystem == HookSubsys_Audio && status.hook_entries[i].installed) {
|
||||||
{
|
|
||||||
if (status.hook_entries[i].subsystem == HookSubsys_Audio && status.hook_entries[i].installed)
|
|
||||||
{
|
|
||||||
audio_hook_on = true;
|
audio_hook_on = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (audio_hook_on)
|
if (audio_hook_on) {
|
||||||
{
|
|
||||||
ImGui::TextDisabled("Game audio also plays locally (echo).");
|
ImGui::TextDisabled("Game audio also plays locally (echo).");
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextDisabled("Audio render-hook is off -> loopback (echo). Enable it in the Injection panel.");
|
ImGui::TextDisabled("Audio render-hook is off -> loopback (echo). Enable it in the Injection panel.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,14 +255,12 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
|
|||||||
// (debug details) shows each stream's format/provenance/activity.
|
// (debug details) shows each stream's format/provenance/activity.
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Text("Render streams: %u", status.audio_streams_seen);
|
ImGui::Text("Render streams: %u", status.audio_streams_seen);
|
||||||
if (status.audio_streams_seen > kMaxAudioStreams)
|
if (status.audio_streams_seen > kMaxAudioStreams) {
|
||||||
{
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextDisabled("(showing first %u)", kMaxAudioStreams);
|
ImGui::TextDisabled("(showing first %u)", kMaxAudioStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!debug_details)
|
if (!debug_details) {
|
||||||
{
|
|
||||||
record_panel_fit("Audio");
|
record_panel_fit("Audio");
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
return; // the per-stream table below is diagnostic detail
|
return; // the per-stream table below is diagnostic detail
|
||||||
@@ -312,9 +269,7 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
|
|||||||
const std::uint32_t rows = std::min<std::uint32_t>(status.audio_streams_seen, kMaxAudioStreams);
|
const std::uint32_t rows = std::min<std::uint32_t>(status.audio_streams_seen, kMaxAudioStreams);
|
||||||
const double now = ImGui::GetTime();
|
const double now = ImGui::GetTime();
|
||||||
const bool resample = (now - rate_base_time_) >= 0.5; // recompute frames/s ~2x a second
|
const bool resample = (now - rate_base_time_) >= 0.5; // recompute frames/s ~2x a second
|
||||||
if (rows > 0 &&
|
if (rows > 0 && ImGui::BeginTable("audio_streams", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) {
|
||||||
ImGui::BeginTable("audio_streams", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit))
|
|
||||||
{
|
|
||||||
ImGui::TableSetupColumn("#");
|
ImGui::TableSetupColumn("#");
|
||||||
ImGui::TableSetupColumn("role");
|
ImGui::TableSetupColumn("role");
|
||||||
ImGui::TableSetupColumn("format");
|
ImGui::TableSetupColumn("format");
|
||||||
@@ -322,23 +277,19 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
|
|||||||
ImGui::TableSetupColumn("frames");
|
ImGui::TableSetupColumn("frames");
|
||||||
ImGui::TableSetupColumn("live");
|
ImGui::TableSetupColumn("live");
|
||||||
ImGui::TableHeadersRow();
|
ImGui::TableHeadersRow();
|
||||||
for (std::uint32_t i = 0; i < rows; ++i)
|
for (std::uint32_t i = 0; i < rows; ++i) {
|
||||||
{
|
|
||||||
const AudioStreamInfo& s = status.audio_streams[i];
|
const AudioStreamInfo& s = status.audio_streams[i];
|
||||||
|
|
||||||
// Debounced activity: remember when this stream last advanced, and call it
|
// Debounced activity: remember when this stream last advanced, and call it
|
||||||
// live for a short window afterwards so bursty releases don't flicker.
|
// live for a short window afterwards so bursty releases don't flicker.
|
||||||
if (s.frames_rendered > prev_frames_[i])
|
if (s.frames_rendered > prev_frames_[i]) {
|
||||||
{
|
|
||||||
last_active_[i] = now;
|
last_active_[i] = now;
|
||||||
}
|
}
|
||||||
prev_frames_[i] = s.frames_rendered;
|
prev_frames_[i] = s.frames_rendered;
|
||||||
const bool live = last_active_[i] > 0.0 && (now - last_active_[i]) < 0.4;
|
const bool live = last_active_[i] > 0.0 && (now - last_active_[i]) < 0.4;
|
||||||
if (resample)
|
if (resample) {
|
||||||
{
|
|
||||||
const double dt = now - rate_base_time_;
|
const double dt = now - rate_base_time_;
|
||||||
frames_per_s_[i] =
|
frames_per_s_[i] = dt > 0.0 ? static_cast<double>(s.frames_rendered - rate_base_frames_[i]) / dt : 0.0;
|
||||||
dt > 0.0 ? static_cast<double>(s.frames_rendered - rate_base_frames_[i]) / dt : 0.0;
|
|
||||||
rate_base_frames_[i] = s.frames_rendered;
|
rate_base_frames_[i] = s.frames_rendered;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,43 +297,35 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
|
|||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
ImGui::Text("%u", i);
|
ImGui::Text("%u", i);
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
ImGui::TextColored(s.is_primary ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) : ImVec4(0.7f, 0.7f, 0.7f, 1.0f),
|
ImGui::TextColored(s.is_primary ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) : ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s",
|
||||||
"%s", s.is_primary ? "primary" : "extra");
|
s.is_primary ? "primary" : "extra");
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
ImGui::Text("%u Hz %uch %u-bit %s", s.sample_rate, s.channels, s.bits,
|
ImGui::Text("%u Hz %uch %u-bit %s", s.sample_rate, s.channels, s.bits, format_tag_name(s.format_tag));
|
||||||
format_tag_name(s.format_tag));
|
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
ImGui::TextColored(audio_format_state_color(s.format_state), "%s",
|
ImGui::TextColored(audio_format_state_color(s.format_state), "%s", audio_format_state_name(s.format_state));
|
||||||
audio_format_state_name(s.format_state));
|
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
ImGui::Text("%llu", static_cast<unsigned long long>(s.frames_rendered));
|
ImGui::Text("%llu", static_cast<unsigned long long>(s.frames_rendered));
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
if (live)
|
if (live) {
|
||||||
{
|
|
||||||
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "live");
|
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "live");
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextDisabled("%6.0f/s", frames_per_s_[i]);
|
ImGui::TextDisabled("%6.0f/s", frames_per_s_[i]);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextDisabled("idle");
|
ImGui::TextDisabled("idle");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::EndTable();
|
ImGui::EndTable();
|
||||||
}
|
}
|
||||||
if (resample)
|
if (resample) {
|
||||||
{
|
|
||||||
rate_base_time_ = now;
|
rate_base_time_ = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Operator controls: re-measure / override the primary stream's format -----
|
// --- Operator controls: re-measure / override the primary stream's format -----
|
||||||
// For when detection is wrong (re-measure) or unrecoverable (override the channels/
|
// For when detection is wrong (re-measure) or unrecoverable (override the channels/
|
||||||
// bit-depth the hook had to assume). Only meaningful while mirroring is active.
|
// bit-depth the hook had to assume). Only meaningful while mirroring is active.
|
||||||
if (running)
|
if (running) {
|
||||||
{
|
|
||||||
ImGui::SeparatorText("Fix the primary stream (debug)");
|
ImGui::SeparatorText("Fix the primary stream (debug)");
|
||||||
if (ImGui::Button("Re-measure rate"))
|
if (ImGui::Button("Re-measure rate")) {
|
||||||
{
|
|
||||||
mirror_.request_op(0, AudioRingOp_Remeasure);
|
mirror_.request_op(0, AudioRingOp_Remeasure);
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
@@ -395,25 +338,26 @@ void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
|
|||||||
ImGui::InputInt("ch", &ov_channels_, 0, 0);
|
ImGui::InputInt("ch", &ov_channels_, 0, 0);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::SetNextItemWidth(90.0f);
|
ImGui::SetNextItemWidth(90.0f);
|
||||||
ImGui::Combo("##ovbits", &ov_bits_idx_, "16-bit\0" "32-bit\0");
|
ImGui::Combo("##ovbits", &ov_bits_idx_,
|
||||||
|
"16-bit\0"
|
||||||
|
"32-bit\0");
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::SetNextItemWidth(80.0f);
|
ImGui::SetNextItemWidth(80.0f);
|
||||||
ImGui::Combo("##ovfmt", &ov_fmt_idx_, "PCM\0" "float\0");
|
ImGui::Combo("##ovfmt", &ov_fmt_idx_,
|
||||||
|
"PCM\0"
|
||||||
|
"float\0");
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Button("Override"))
|
if (ImGui::Button("Override")) {
|
||||||
{
|
|
||||||
ov_rate_ = std::clamp(ov_rate_, 8000, 384000);
|
ov_rate_ = std::clamp(ov_rate_, 8000, 384000);
|
||||||
ov_channels_ = std::clamp(ov_channels_, 1, 8);
|
ov_channels_ = std::clamp(ov_channels_, 1, 8);
|
||||||
const std::uint32_t bits = ov_bits_idx_ == 0 ? 16u : 32u;
|
const std::uint32_t bits = ov_bits_idx_ == 0 ? 16u : 32u;
|
||||||
const std::uint32_t tag =
|
const std::uint32_t tag = ov_fmt_idx_ == 1 ? static_cast<std::uint32_t>(WAVE_FORMAT_IEEE_FLOAT)
|
||||||
ov_fmt_idx_ == 1 ? static_cast<std::uint32_t>(WAVE_FORMAT_IEEE_FLOAT)
|
|
||||||
: static_cast<std::uint32_t>(WAVE_FORMAT_PCM);
|
: static_cast<std::uint32_t>(WAVE_FORMAT_PCM);
|
||||||
const AudioFormatOverride fmt{static_cast<std::uint32_t>(ov_rate_),
|
const AudioFormatOverride fmt{static_cast<std::uint32_t>(ov_rate_),
|
||||||
static_cast<std::uint32_t>(ov_channels_), bits, tag};
|
static_cast<std::uint32_t>(ov_channels_), bits, tag};
|
||||||
mirror_.request_op(0, AudioRingOp_Override, fmt.rate, fmt.channels, fmt.bits, fmt.format_tag);
|
mirror_.request_op(0, AudioRingOp_Override, fmt.rate, fmt.channels, fmt.bits, fmt.format_tag);
|
||||||
// Persist it for this game so the correction sticks across launches.
|
// Persist it for this game so the correction sticks across launches.
|
||||||
if (!target_image_.empty())
|
if (!target_image_.empty()) {
|
||||||
{
|
|
||||||
overrides_.set(target_image_, fmt);
|
overrides_.set(target_image_, fmt);
|
||||||
override_applied_ = true; // don't let manage_overrides re-apply an older saved value
|
override_applied_ = true; // don't let manage_overrides re-apply an older saved value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,30 +13,19 @@
|
|||||||
#include "audio/audio_overrides.hpp"
|
#include "audio/audio_overrides.hpp"
|
||||||
#include "ipc/ipc_server.hpp"
|
#include "ipc/ipc_server.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
class AudioPanel
|
class AudioPanel {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
AudioPanel()
|
AudioPanel() { overrides_.load(); }
|
||||||
{
|
|
||||||
overrides_.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The window whose process audio to mirror (0 if none); typically the
|
// The window whose process audio to mirror (0 if none); typically the
|
||||||
// injected game's HWND.
|
// injected game's HWND.
|
||||||
void set_target(HWND target)
|
void set_target(HWND target) { target_ = target; }
|
||||||
{
|
|
||||||
target_ = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wire a sink for host-side log lines (override-overwrite warnings etc.). main
|
// Wire a sink for host-side log lines (override-overwrite warnings etc.). main
|
||||||
// connects this to the injection panel's Log-window channel.
|
// connects this to the injection panel's Log-window channel.
|
||||||
void set_logger(std::function<void(std::uint32_t, const char*)> logger)
|
void set_logger(std::function<void(std::uint32_t, const char*)> logger) { logger_ = std::move(logger); }
|
||||||
{
|
|
||||||
logger_ = std::move(logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
// `status` is the hook's back-channel, for the render-stream view. With
|
// `status` is the hook's back-channel, for the render-stream view. With
|
||||||
// `debug_details` on, the per-stream table is shown.
|
// `debug_details` on, the per-stream table is shown.
|
||||||
@@ -46,47 +35,23 @@ public:
|
|||||||
// loopback mirror were running with long status/reason strings -- without a live
|
// loopback mirror were running with long status/reason strings -- without a live
|
||||||
// AudioMirror, so the fit test can measure the panel's worst-case size. Never set in
|
// AudioMirror, so the fit test can measure the panel's worst-case size. Never set in
|
||||||
// the shipping host (the render path is identical, just fed synthetic values).
|
// the shipping host (the render path is identical, just fed synthetic values).
|
||||||
void dev_set_demo(bool on)
|
void dev_set_demo(bool on) { demo_ = on; }
|
||||||
{
|
|
||||||
demo_ = on;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef COOP_TEST_HARNESS
|
#ifdef COOP_TEST_HARNESS
|
||||||
// Test-harness hooks (debug builds only): drive the real audio code paths and read
|
// Test-harness hooks (debug builds only): drive the real audio code paths and read
|
||||||
// state back, incl. targeting a windowless process by pid (coop_tone has no window).
|
// state back, incl. targeting a windowless process by pid (coop_tone has no window).
|
||||||
void dev_set_enabled(bool on)
|
void dev_set_enabled(bool on) { enabled_ = on; }
|
||||||
{
|
void dev_set_pid(DWORD pid) { dev_pid_ = pid; }
|
||||||
enabled_ = on;
|
void dev_request_op(unsigned slot, std::uint32_t kind, std::uint32_t rate, std::uint32_t ch, std::uint32_t bits,
|
||||||
}
|
std::uint32_t tag)
|
||||||
void dev_set_pid(DWORD pid)
|
|
||||||
{
|
|
||||||
dev_pid_ = pid;
|
|
||||||
}
|
|
||||||
void dev_request_op(unsigned slot, std::uint32_t kind, std::uint32_t rate, std::uint32_t ch,
|
|
||||||
std::uint32_t bits, std::uint32_t tag)
|
|
||||||
{
|
{
|
||||||
mirror_.request_op(slot, kind, rate, ch, bits, tag);
|
mirror_.request_op(slot, kind, rate, ch, bits, tag);
|
||||||
}
|
}
|
||||||
[[nodiscard]] bool dev_running() const
|
[[nodiscard]] bool dev_running() const { return mirror_.running(); }
|
||||||
{
|
[[nodiscard]] unsigned dev_rate() const { return mirror_.sample_rate(); }
|
||||||
return mirror_.running();
|
[[nodiscard]] unsigned dev_channels() const { return mirror_.channels(); }
|
||||||
}
|
[[nodiscard]] std::string dev_source() const { return mirror_.source_name(); }
|
||||||
[[nodiscard]] unsigned dev_rate() const
|
[[nodiscard]] std::string dev_reason() const { return mirror_.fallback_reason(); }
|
||||||
{
|
|
||||||
return mirror_.sample_rate();
|
|
||||||
}
|
|
||||||
[[nodiscard]] unsigned dev_channels() const
|
|
||||||
{
|
|
||||||
return mirror_.channels();
|
|
||||||
}
|
|
||||||
[[nodiscard]] std::string dev_source() const
|
|
||||||
{
|
|
||||||
return mirror_.source_name();
|
|
||||||
}
|
|
||||||
[[nodiscard]] std::string dev_reason() const
|
|
||||||
{
|
|
||||||
return mirror_.fallback_reason();
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
|
|
||||||
#include <dxgiformat.h>
|
#include <dxgiformat.h>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// Map an sRGB DXGI format to its plain UNORM sibling (same byte layout / type
|
// Map an sRGB DXGI format to its plain UNORM sibling (same byte layout / type
|
||||||
// group), leaving non-sRGB formats unchanged.
|
// group), leaving non-sRGB formats unchanged.
|
||||||
@@ -19,8 +18,7 @@ namespace coop
|
|||||||
// (the producer's sRGB texture -> the host's UNORM copy) is allowed.
|
// (the producer's sRGB texture -> the host's UNORM copy) is allowed.
|
||||||
inline DXGI_FORMAT srgb_to_unorm(DXGI_FORMAT format)
|
inline DXGI_FORMAT srgb_to_unorm(DXGI_FORMAT format)
|
||||||
{
|
{
|
||||||
switch (format)
|
switch (format) {
|
||||||
{
|
|
||||||
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
|
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
|
||||||
return DXGI_FORMAT_R8G8B8A8_UNORM;
|
return DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||||
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
|
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
|
||||||
|
|||||||
@@ -6,11 +6,9 @@
|
|||||||
|
|
||||||
using Microsoft::WRL::ComPtr;
|
using Microsoft::WRL::ComPtr;
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
// Fullscreen triangle generated from SV_VertexID -- no vertex/index buffers
|
// Fullscreen triangle generated from SV_VertexID -- no vertex/index buffers
|
||||||
// needed. Samples the source texture across the [0,1] UV range.
|
// needed. Samples the source texture across the [0,1] UV range.
|
||||||
@@ -38,10 +36,10 @@ ComPtr<ID3DBlob> compile(const char* entry, const char* target)
|
|||||||
{
|
{
|
||||||
ComPtr<ID3DBlob> blob;
|
ComPtr<ID3DBlob> blob;
|
||||||
ComPtr<ID3DBlob> errors;
|
ComPtr<ID3DBlob> errors;
|
||||||
const HRESULT hr = D3DCompile(kShaderSource, sizeof(kShaderSource) - 1, "frame_renderer", nullptr, nullptr, entry,
|
const HRESULT hr =
|
||||||
target, D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, blob.GetAddressOf(), errors.GetAddressOf());
|
D3DCompile(kShaderSource, sizeof(kShaderSource) - 1, "frame_renderer", nullptr, nullptr, entry, target,
|
||||||
if (FAILED(hr))
|
D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, blob.GetAddressOf(), errors.GetAddressOf());
|
||||||
{
|
if (FAILED(hr)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return blob;
|
return blob;
|
||||||
@@ -53,18 +51,15 @@ bool FrameRenderer::init(ID3D11Device* device)
|
|||||||
{
|
{
|
||||||
ComPtr<ID3DBlob> vs_blob = compile("vs_main", "vs_5_0");
|
ComPtr<ID3DBlob> vs_blob = compile("vs_main", "vs_5_0");
|
||||||
ComPtr<ID3DBlob> ps_blob = compile("ps_main", "ps_5_0");
|
ComPtr<ID3DBlob> ps_blob = compile("ps_main", "ps_5_0");
|
||||||
if (vs_blob == nullptr || ps_blob == nullptr)
|
if (vs_blob == nullptr || ps_blob == nullptr) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (FAILED(device->CreateVertexShader(vs_blob->GetBufferPointer(), vs_blob->GetBufferSize(), nullptr,
|
if (FAILED(device->CreateVertexShader(vs_blob->GetBufferPointer(), vs_blob->GetBufferSize(), nullptr,
|
||||||
vs_.GetAddressOf())))
|
vs_.GetAddressOf()))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (FAILED(device->CreatePixelShader(ps_blob->GetBufferPointer(), ps_blob->GetBufferSize(), nullptr,
|
if (FAILED(device->CreatePixelShader(ps_blob->GetBufferPointer(), ps_blob->GetBufferSize(), nullptr,
|
||||||
ps_.GetAddressOf())))
|
ps_.GetAddressOf()))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +69,7 @@ bool FrameRenderer::init(ID3D11Device* device)
|
|||||||
sd.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
|
sd.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||||
sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
|
sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||||
sd.ComparisonFunc = D3D11_COMPARISON_NEVER;
|
sd.ComparisonFunc = D3D11_COMPARISON_NEVER;
|
||||||
if (FAILED(device->CreateSamplerState(&sd, sampler_.GetAddressOf())))
|
if (FAILED(device->CreateSamplerState(&sd, sampler_.GetAddressOf()))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -84,8 +78,7 @@ bool FrameRenderer::init(ID3D11Device* device)
|
|||||||
void FrameRenderer::draw(ID3D11DeviceContext* ctx, ID3D11ShaderResourceView* srv, std::uint32_t src_w,
|
void FrameRenderer::draw(ID3D11DeviceContext* ctx, ID3D11ShaderResourceView* srv, std::uint32_t src_w,
|
||||||
std::uint32_t src_h, std::uint32_t dst_w, std::uint32_t dst_h)
|
std::uint32_t src_h, std::uint32_t dst_w, std::uint32_t dst_h)
|
||||||
{
|
{
|
||||||
if (srv == nullptr || src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0)
|
if (srv == nullptr || src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,9 @@
|
|||||||
#include <d3d11.h>
|
#include <d3d11.h>
|
||||||
#include <wrl/client.h>
|
#include <wrl/client.h>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
class FrameRenderer
|
class FrameRenderer {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
bool init(ID3D11Device* device);
|
bool init(ID3D11Device* device);
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
|
|
||||||
#include <windows.h> // HRESULT, S_OK, WAIT_ABANDONED, WAIT_TIMEOUT
|
#include <windows.h> // HRESULT, S_OK, WAIT_ABANDONED, WAIT_TIMEOUT
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// True when an IDXGIKeyedMutex::AcquireSync result means we now HOLD the mutex and must copy + then
|
// True when an IDXGIKeyedMutex::AcquireSync result means we now HOLD the mutex and must copy + then
|
||||||
// release it. S_OK is the normal case. WAIT_ABANDONED is success-with-recovery: a previous owner
|
// release it. S_OK is the normal case. WAIT_ABANDONED is success-with-recovery: a previous owner
|
||||||
|
|||||||
@@ -5,13 +5,11 @@
|
|||||||
#include "coop/protocol.hpp"
|
#include "coop/protocol.hpp"
|
||||||
#include "coop/shared_memory.hpp"
|
#include "coop/shared_memory.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
bool SharedTextureSource::init(ID3D11Device* device)
|
bool SharedTextureSource::init(ID3D11Device* device)
|
||||||
{
|
{
|
||||||
if (device == nullptr || FAILED(device->QueryInterface(IID_PPV_ARGS(&device_))))
|
if (device == nullptr || FAILED(device->QueryInterface(IID_PPV_ARGS(&device_)))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
device_->GetImmediateContext(&ctx_);
|
device_->GetImmediateContext(&ctx_);
|
||||||
@@ -40,21 +38,17 @@ bool SharedTextureSource::reopen(unsigned long pid, const VideoShareView& share)
|
|||||||
width_ = height_ = format_ = 0;
|
width_ = height_ = format_ = 0;
|
||||||
pid_ = pid;
|
pid_ = pid;
|
||||||
|
|
||||||
if (pid == 0 || share.width == 0 || share.height == 0)
|
if (pid == 0 || share.width == 0 || share.height == 0) {
|
||||||
{
|
|
||||||
return false; // the hook hasn't shared a backbuffer yet
|
return false; // the hook hasn't shared a backbuffer yet
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::wstring name = video_share_name(pid);
|
const std::wstring name = video_share_name(pid);
|
||||||
if (FAILED(device_->OpenSharedResourceByName(name.c_str(),
|
if (FAILED(device_->OpenSharedResourceByName(name.c_str(), DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE,
|
||||||
DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE,
|
IID_PPV_ARGS(&shared_)))
|
||||||
IID_PPV_ARGS(&shared_))) ||
|
|| shared_ == nullptr) {
|
||||||
shared_ == nullptr)
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (FAILED(shared_.As(&mutex_)) || mutex_ == nullptr)
|
if (FAILED(shared_.As(&mutex_)) || mutex_ == nullptr) {
|
||||||
{
|
|
||||||
shared_.Reset();
|
shared_.Reset();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -72,14 +66,12 @@ bool SharedTextureSource::reopen(unsigned long pid, const VideoShareView& share)
|
|||||||
desc.SampleDesc.Count = 1;
|
desc.SampleDesc.Count = 1;
|
||||||
desc.Usage = D3D11_USAGE_DEFAULT;
|
desc.Usage = D3D11_USAGE_DEFAULT;
|
||||||
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
||||||
if (FAILED(device_->CreateTexture2D(&desc, nullptr, &private_)) || private_ == nullptr)
|
if (FAILED(device_->CreateTexture2D(&desc, nullptr, &private_)) || private_ == nullptr) {
|
||||||
{
|
|
||||||
mutex_.Reset();
|
mutex_.Reset();
|
||||||
shared_.Reset();
|
shared_.Reset();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (FAILED(device_->CreateShaderResourceView(private_.Get(), nullptr, &srv_)))
|
if (FAILED(device_->CreateShaderResourceView(private_.Get(), nullptr, &srv_))) {
|
||||||
{
|
|
||||||
srv_.Reset();
|
srv_.Reset();
|
||||||
private_.Reset();
|
private_.Reset();
|
||||||
mutex_.Reset();
|
mutex_.Reset();
|
||||||
@@ -96,8 +88,7 @@ bool SharedTextureSource::reopen(unsigned long pid, const VideoShareView& share)
|
|||||||
bool SharedTextureSource::map_staging_copy(Microsoft::WRL::ComPtr<ID3D11Texture2D>& staging,
|
bool SharedTextureSource::map_staging_copy(Microsoft::WRL::ComPtr<ID3D11Texture2D>& staging,
|
||||||
D3D11_MAPPED_SUBRESOURCE& map, D3D11_TEXTURE2D_DESC& desc)
|
D3D11_MAPPED_SUBRESOURCE& map, D3D11_TEXTURE2D_DESC& desc)
|
||||||
{
|
{
|
||||||
if (private_ == nullptr || ctx_ == nullptr || device_ == nullptr)
|
if (private_ == nullptr || ctx_ == nullptr || device_ == nullptr) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
private_->GetDesc(&desc);
|
private_->GetDesc(&desc);
|
||||||
@@ -106,8 +97,7 @@ bool SharedTextureSource::map_staging_copy(Microsoft::WRL::ComPtr<ID3D11Texture2
|
|||||||
staging_desc.BindFlags = 0;
|
staging_desc.BindFlags = 0;
|
||||||
staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||||
staging_desc.MiscFlags = 0;
|
staging_desc.MiscFlags = 0;
|
||||||
if (FAILED(device_->CreateTexture2D(&staging_desc, nullptr, &staging)))
|
if (FAILED(device_->CreateTexture2D(&staging_desc, nullptr, &staging))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ctx_->CopyResource(staging.Get(), private_.Get());
|
ctx_->CopyResource(staging.Get(), private_.Get());
|
||||||
@@ -119,15 +109,13 @@ bool SharedTextureSource::read_frame(std::vector<std::uint8_t>& out, std::uint32
|
|||||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging;
|
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging;
|
||||||
D3D11_MAPPED_SUBRESOURCE map{};
|
D3D11_MAPPED_SUBRESOURCE map{};
|
||||||
D3D11_TEXTURE2D_DESC desc{};
|
D3D11_TEXTURE2D_DESC desc{};
|
||||||
if (!map_staging_copy(staging, map, desc))
|
if (!map_staging_copy(staging, map, desc)) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
w = desc.Width;
|
w = desc.Width;
|
||||||
h = desc.Height;
|
h = desc.Height;
|
||||||
out.resize(static_cast<std::size_t>(w) * h * 4);
|
out.resize(static_cast<std::size_t>(w) * h * 4);
|
||||||
for (std::uint32_t y = 0; y < h; ++y)
|
for (std::uint32_t y = 0; y < h; ++y) {
|
||||||
{
|
|
||||||
memcpy(out.data() + static_cast<std::size_t>(y) * w * 4,
|
memcpy(out.data() + static_cast<std::size_t>(y) * w * 4,
|
||||||
static_cast<const std::uint8_t*>(map.pData) + static_cast<std::size_t>(y) * map.RowPitch,
|
static_cast<const std::uint8_t*>(map.pData) + static_cast<std::size_t>(y) * map.RowPitch,
|
||||||
static_cast<std::size_t>(w) * 4);
|
static_cast<std::size_t>(w) * 4);
|
||||||
@@ -141,17 +129,15 @@ bool SharedTextureSource::read_pixel(std::uint32_t x, std::uint32_t y, std::uint
|
|||||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging;
|
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging;
|
||||||
D3D11_MAPPED_SUBRESOURCE map{};
|
D3D11_MAPPED_SUBRESOURCE map{};
|
||||||
D3D11_TEXTURE2D_DESC desc{};
|
D3D11_TEXTURE2D_DESC desc{};
|
||||||
if (!map_staging_copy(staging, map, desc))
|
if (!map_staging_copy(staging, map, desc)) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (x >= desc.Width || y >= desc.Height)
|
if (x >= desc.Width || y >= desc.Height) {
|
||||||
{
|
|
||||||
ctx_->Unmap(staging.Get(), 0);
|
ctx_->Unmap(staging.Get(), 0);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto* px = static_cast<const std::uint8_t*>(map.pData) + static_cast<std::size_t>(y) * map.RowPitch +
|
const auto* px = static_cast<const std::uint8_t*>(map.pData) + static_cast<std::size_t>(y) * map.RowPitch
|
||||||
static_cast<std::size_t>(x) * 4; // R8G8B8A8_UNORM
|
+ static_cast<std::size_t>(x) * 4; // R8G8B8A8_UNORM
|
||||||
out[0] = px[0];
|
out[0] = px[0];
|
||||||
out[1] = px[1];
|
out[1] = px[1];
|
||||||
out[2] = px[2];
|
out[2] = px[2];
|
||||||
@@ -162,43 +148,36 @@ bool SharedTextureSource::read_pixel(std::uint32_t x, std::uint32_t y, std::uint
|
|||||||
|
|
||||||
bool SharedTextureSource::update(const VideoShareView& share, unsigned long pid)
|
bool SharedTextureSource::update(const VideoShareView& share, unsigned long pid)
|
||||||
{
|
{
|
||||||
if (device_ == nullptr || pid == 0)
|
if (device_ == nullptr || pid == 0) {
|
||||||
{
|
|
||||||
reset();
|
reset();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// (Re)open whenever the target or the published backbuffer geometry changes.
|
// (Re)open whenever the target or the published backbuffer geometry changes.
|
||||||
if (pid != pid_ || share.width != width_ || share.height != height_ || share.format != format_)
|
if (pid != pid_ || share.width != width_ || share.height != height_ || share.format != format_) {
|
||||||
{
|
if (!reopen(pid, share)) {
|
||||||
if (!reopen(pid, share))
|
|
||||||
{
|
|
||||||
return srv_ != nullptr; // couldn't open yet; keep any prior frame
|
return srv_ != nullptr; // couldn't open yet; keep any prior frame
|
||||||
}
|
}
|
||||||
last_generation_ = 0; // force a copy of the current frame
|
last_generation_ = 0; // force a copy of the current frame
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shared_ == nullptr || mutex_ == nullptr)
|
if (shared_ == nullptr || mutex_ == nullptr) {
|
||||||
{
|
|
||||||
return srv_ != nullptr;
|
return srv_ != nullptr;
|
||||||
}
|
}
|
||||||
if (share.generation == last_generation_)
|
if (share.generation == last_generation_) {
|
||||||
{
|
|
||||||
return srv_ != nullptr; // no new frame; keep showing the last copy
|
return srv_ != nullptr; // no new frame; keep showing the last copy
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bounded wait so a stalled producer can't hang the host's render thread. WAIT_ABANDONED (a prior
|
// Bounded wait so a stalled producer can't hang the host's render thread. WAIT_ABANDONED (a prior
|
||||||
// owner died holding the mutex -- e.g. a host that crashed and reconnected) counts as acquired:
|
// owner died holding the mutex -- e.g. a host that crashed and reconnected) counts as acquired:
|
||||||
// recover by copying + releasing rather than skipping, which would hold it forever and freeze.
|
// recover by copying + releasing rather than skipping, which would hold it forever and freeze.
|
||||||
if (keyed_mutex_acquired(mutex_->AcquireSync(kVideoMutexKey, 8)))
|
if (keyed_mutex_acquired(mutex_->AcquireSync(kVideoMutexKey, 8))) {
|
||||||
{
|
|
||||||
ctx_->CopyResource(private_.Get(), shared_.Get());
|
ctx_->CopyResource(private_.Get(), shared_.Get());
|
||||||
mutex_->ReleaseSync(kVideoMutexKey);
|
mutex_->ReleaseSync(kVideoMutexKey);
|
||||||
// Generations between the last copy and this one were published but never shown
|
// Generations between the last copy and this one were published but never shown
|
||||||
// (we only ever copy the newest). last_generation_ == 0 is the first copy after a
|
// (we only ever copy the newest). last_generation_ == 0 is the first copy after a
|
||||||
// (re)open, where the gap to a large generation is meaningless, so skip it.
|
// (re)open, where the gap to a large generation is meaningless, so skip it.
|
||||||
if (last_generation_ != 0 && share.generation > last_generation_ + 1)
|
if (last_generation_ != 0 && share.generation > last_generation_ + 1) {
|
||||||
{
|
|
||||||
frames_missed_ += share.generation - last_generation_ - 1;
|
frames_missed_ += share.generation - last_generation_ - 1;
|
||||||
}
|
}
|
||||||
last_generation_ = share.generation;
|
last_generation_ = share.generation;
|
||||||
|
|||||||
@@ -14,11 +14,9 @@
|
|||||||
|
|
||||||
#include "ipc/ipc_server.hpp"
|
#include "ipc/ipc_server.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
class SharedTextureSource
|
class SharedTextureSource {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
// Binds to the host's device (must support ID3D11Device1). Returns false if not.
|
// Binds to the host's device (must support ID3D11Device1). Returns false if not.
|
||||||
bool init(ID3D11Device* device);
|
bool init(ID3D11Device* device);
|
||||||
@@ -43,28 +41,13 @@ public:
|
|||||||
// Returns false if no frame has been copied yet or the readback failed.
|
// Returns false if no frame has been copied yet or the readback failed.
|
||||||
bool read_pixel(std::uint32_t x, std::uint32_t y, std::uint8_t out[4]);
|
bool read_pixel(std::uint32_t x, std::uint32_t y, std::uint8_t out[4]);
|
||||||
|
|
||||||
[[nodiscard]] ID3D11ShaderResourceView* srv() const
|
[[nodiscard]] ID3D11ShaderResourceView* srv() const { return srv_.Get(); }
|
||||||
{
|
[[nodiscard]] std::uint32_t width() const { return width_; }
|
||||||
return srv_.Get();
|
[[nodiscard]] std::uint32_t height() const { return height_; }
|
||||||
}
|
[[nodiscard]] std::uint64_t frames_copied() const { return frames_copied_; }
|
||||||
[[nodiscard]] std::uint32_t width() const
|
|
||||||
{
|
|
||||||
return width_;
|
|
||||||
}
|
|
||||||
[[nodiscard]] std::uint32_t height() const
|
|
||||||
{
|
|
||||||
return height_;
|
|
||||||
}
|
|
||||||
[[nodiscard]] std::uint64_t frames_copied() const
|
|
||||||
{
|
|
||||||
return frames_copied_;
|
|
||||||
}
|
|
||||||
// Cumulative published frames the host never displayed because the generation
|
// Cumulative published frames the host never displayed because the generation
|
||||||
// advanced by more than one between copies (host render rate < hook publish rate).
|
// advanced by more than one between copies (host render rate < hook publish rate).
|
||||||
[[nodiscard]] std::uint64_t frames_missed() const
|
[[nodiscard]] std::uint64_t frames_missed() const { return frames_missed_; }
|
||||||
{
|
|
||||||
return frames_missed_;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool reopen(unsigned long pid, const VideoShareView& share);
|
bool reopen(unsigned long pid, const VideoShareView& share);
|
||||||
|
|||||||
@@ -12,19 +12,16 @@
|
|||||||
|
|
||||||
using Microsoft::WRL::ComPtr;
|
using Microsoft::WRL::ComPtr;
|
||||||
|
|
||||||
namespace winrt
|
namespace winrt {
|
||||||
{
|
|
||||||
using namespace Windows::Graphics;
|
using namespace Windows::Graphics;
|
||||||
using namespace Windows::Graphics::Capture;
|
using namespace Windows::Graphics::Capture;
|
||||||
using namespace Windows::Graphics::DirectX;
|
using namespace Windows::Graphics::DirectX;
|
||||||
using namespace Windows::Graphics::DirectX::Direct3D11;
|
using namespace Windows::Graphics::DirectX::Direct3D11;
|
||||||
} // namespace winrt
|
} // namespace winrt
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
constexpr auto kPixelFormat = winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized;
|
constexpr auto kPixelFormat = winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized;
|
||||||
|
|
||||||
@@ -33,8 +30,7 @@ ComPtr<ID3D11Texture2D> texture_from_surface(winrt::IDirect3DSurface const& surf
|
|||||||
{
|
{
|
||||||
auto access = surface.as<::Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess>();
|
auto access = surface.as<::Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess>();
|
||||||
ComPtr<ID3D11Texture2D> texture;
|
ComPtr<ID3D11Texture2D> texture;
|
||||||
if (access)
|
if (access) {
|
||||||
{
|
|
||||||
access->GetInterface(__uuidof(ID3D11Texture2D), reinterpret_cast<void**>(texture.GetAddressOf()));
|
access->GetInterface(__uuidof(ID3D11Texture2D), reinterpret_cast<void**>(texture.GetAddressOf()));
|
||||||
}
|
}
|
||||||
return texture;
|
return texture;
|
||||||
@@ -50,24 +46,20 @@ WindowCapture::~WindowCapture()
|
|||||||
bool WindowCapture::start(HWND target, ID3D11Device* device)
|
bool WindowCapture::start(HWND target, ID3D11Device* device)
|
||||||
{
|
{
|
||||||
stop();
|
stop();
|
||||||
if (target == nullptr || device == nullptr || !IsWindow(target))
|
if (target == nullptr || device == nullptr || !IsWindow(target)) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try {
|
||||||
{
|
|
||||||
device_ = device;
|
device_ = device;
|
||||||
|
|
||||||
// Wrap our D3D11 device as the WinRT device the frame pool renders on.
|
// Wrap our D3D11 device as the WinRT device the frame pool renders on.
|
||||||
ComPtr<IDXGIDevice> dxgi_device;
|
ComPtr<IDXGIDevice> dxgi_device;
|
||||||
if (FAILED(device->QueryInterface(IID_PPV_ARGS(dxgi_device.GetAddressOf()))))
|
if (FAILED(device->QueryInterface(IID_PPV_ARGS(dxgi_device.GetAddressOf())))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
winrt::com_ptr<::IInspectable> inspectable;
|
winrt::com_ptr<::IInspectable> inspectable;
|
||||||
if (FAILED(CreateDirect3D11DeviceFromDXGIDevice(dxgi_device.Get(), inspectable.put())))
|
if (FAILED(CreateDirect3D11DeviceFromDXGIDevice(dxgi_device.Get(), inspectable.put()))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
winrt_device_ = inspectable.as<winrt::IDirect3DDevice>();
|
winrt_device_ = inspectable.as<winrt::IDirect3DDevice>();
|
||||||
@@ -75,40 +67,30 @@ bool WindowCapture::start(HWND target, ID3D11Device* device)
|
|||||||
// Create a capture item for the target window via the interop factory.
|
// Create a capture item for the target window via the interop factory.
|
||||||
auto interop = winrt::get_activation_factory<winrt::GraphicsCaptureItem, ::IGraphicsCaptureItemInterop>();
|
auto interop = winrt::get_activation_factory<winrt::GraphicsCaptureItem, ::IGraphicsCaptureItemInterop>();
|
||||||
if (FAILED(interop->CreateForWindow(target, winrt::guid_of<winrt::GraphicsCaptureItem>(),
|
if (FAILED(interop->CreateForWindow(target, winrt::guid_of<winrt::GraphicsCaptureItem>(),
|
||||||
winrt::put_abi(item_))))
|
winrt::put_abi(item_)))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pool_size_ = item_.Size();
|
pool_size_ = item_.Size();
|
||||||
frame_pool_ =
|
frame_pool_ = winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(winrt_device_, kPixelFormat, 2, pool_size_);
|
||||||
winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(winrt_device_, kPixelFormat, 2, pool_size_);
|
|
||||||
session_ = frame_pool_.CreateCaptureSession(item_);
|
session_ = frame_pool_.CreateCaptureSession(item_);
|
||||||
frame_token_ = frame_pool_.FrameArrived({this, &WindowCapture::on_frame_arrived});
|
frame_token_ = frame_pool_.FrameArrived({this, &WindowCapture::on_frame_arrived});
|
||||||
|
|
||||||
// Best-effort: hide the cursor and the yellow capture border (the border
|
// Best-effort: hide the cursor and the yellow capture border (the border
|
||||||
// API requires a recent Windows build, hence the guard).
|
// API requires a recent Windows build, hence the guard).
|
||||||
try
|
try {
|
||||||
{
|
|
||||||
session_.IsCursorCaptureEnabled(false);
|
session_.IsCursorCaptureEnabled(false);
|
||||||
|
} catch (...) {
|
||||||
}
|
}
|
||||||
catch (...)
|
try {
|
||||||
{
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
session_.IsBorderRequired(false);
|
session_.IsBorderRequired(false);
|
||||||
}
|
} catch (...) {
|
||||||
catch (...)
|
|
||||||
{
|
|
||||||
}
|
}
|
||||||
|
|
||||||
session_.StartCapture();
|
session_.StartCapture();
|
||||||
target_ = target;
|
target_ = target;
|
||||||
return true;
|
return true;
|
||||||
}
|
} catch (...) {
|
||||||
catch (...)
|
|
||||||
{
|
|
||||||
stop();
|
stop();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -116,18 +98,15 @@ bool WindowCapture::start(HWND target, ID3D11Device* device)
|
|||||||
|
|
||||||
void WindowCapture::stop()
|
void WindowCapture::stop()
|
||||||
{
|
{
|
||||||
if (frame_pool_ != nullptr && frame_token_)
|
if (frame_pool_ != nullptr && frame_token_) {
|
||||||
{
|
|
||||||
frame_pool_.FrameArrived(frame_token_);
|
frame_pool_.FrameArrived(frame_token_);
|
||||||
frame_token_ = {};
|
frame_token_ = {};
|
||||||
}
|
}
|
||||||
if (session_ != nullptr)
|
if (session_ != nullptr) {
|
||||||
{
|
|
||||||
session_.Close();
|
session_.Close();
|
||||||
session_ = nullptr;
|
session_ = nullptr;
|
||||||
}
|
}
|
||||||
if (frame_pool_ != nullptr)
|
if (frame_pool_ != nullptr) {
|
||||||
{
|
|
||||||
frame_pool_.Close();
|
frame_pool_.Close();
|
||||||
frame_pool_ = nullptr;
|
frame_pool_ = nullptr;
|
||||||
}
|
}
|
||||||
@@ -152,8 +131,7 @@ void WindowCapture::on_frame_arrived(winrt::Direct3D11CaptureFramePool const& po
|
|||||||
auto frame = pool.TryGetNextFrame();
|
auto frame = pool.TryGetNextFrame();
|
||||||
std::lock_guard<std::mutex> lock(mutex_);
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
++frames_arrived_; // capture-rate metric (this is the WGC delivery cadence)
|
++frames_arrived_; // capture-rate metric (this is the WGC delivery cadence)
|
||||||
if (pending_ != nullptr)
|
if (pending_ != nullptr) {
|
||||||
{
|
|
||||||
pending_.Close(); // drop the un-consumed previous frame back to the pool
|
pending_.Close(); // drop the un-consumed previous frame back to the pool
|
||||||
}
|
}
|
||||||
pending_ = frame;
|
pending_ = frame;
|
||||||
@@ -169,15 +147,12 @@ void WindowCapture::draw_latest(FrameRenderer& renderer, ID3D11DeviceContext* ct
|
|||||||
pending_ = nullptr;
|
pending_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frame != nullptr)
|
if (frame != nullptr) {
|
||||||
{
|
if (ComPtr<ID3D11Texture2D> src = texture_from_surface(frame.Surface())) {
|
||||||
if (ComPtr<ID3D11Texture2D> src = texture_from_surface(frame.Surface()))
|
|
||||||
{
|
|
||||||
D3D11_TEXTURE2D_DESC desc = {};
|
D3D11_TEXTURE2D_DESC desc = {};
|
||||||
src->GetDesc(&desc);
|
src->GetDesc(&desc);
|
||||||
|
|
||||||
if (latest_ == nullptr || desc.Width != width_ || desc.Height != height_)
|
if (latest_ == nullptr || desc.Width != width_ || desc.Height != height_) {
|
||||||
{
|
|
||||||
latest_srv_.Reset();
|
latest_srv_.Reset();
|
||||||
latest_.Reset();
|
latest_.Reset();
|
||||||
|
|
||||||
@@ -186,16 +161,12 @@ void WindowCapture::draw_latest(FrameRenderer& renderer, ID3D11DeviceContext* ct
|
|||||||
dst.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
dst.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
||||||
dst.CPUAccessFlags = 0;
|
dst.CPUAccessFlags = 0;
|
||||||
dst.MiscFlags = 0;
|
dst.MiscFlags = 0;
|
||||||
if (SUCCEEDED(device_->CreateTexture2D(&dst, nullptr, latest_.GetAddressOf())))
|
if (SUCCEEDED(device_->CreateTexture2D(&dst, nullptr, latest_.GetAddressOf()))) {
|
||||||
{
|
if (SUCCEEDED(
|
||||||
if (SUCCEEDED(device_->CreateShaderResourceView(latest_.Get(), nullptr,
|
device_->CreateShaderResourceView(latest_.Get(), nullptr, latest_srv_.GetAddressOf()))) {
|
||||||
latest_srv_.GetAddressOf())))
|
|
||||||
{
|
|
||||||
width_ = desc.Width;
|
width_ = desc.Width;
|
||||||
height_ = desc.Height;
|
height_ = desc.Height;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
// Drop the texture so the (latest_ == nullptr) guard retries next frame instead
|
// Drop the texture so the (latest_ == nullptr) guard retries next frame instead
|
||||||
// of leaving a null SRV (a silently black mirror) until the next resize.
|
// of leaving a null SRV (a silently black mirror) until the next resize.
|
||||||
latest_.Reset();
|
latest_.Reset();
|
||||||
@@ -203,8 +174,7 @@ void WindowCapture::draw_latest(FrameRenderer& renderer, ID3D11DeviceContext* ct
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (latest_ != nullptr)
|
if (latest_ != nullptr) {
|
||||||
{
|
|
||||||
ctx->CopyResource(latest_.Get(), src.Get());
|
ctx->CopyResource(latest_.Get(), src.Get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,15 +182,13 @@ void WindowCapture::draw_latest(FrameRenderer& renderer, ID3D11DeviceContext* ct
|
|||||||
|
|
||||||
// If the window resized, the capture item changes size; re-fit the pool.
|
// If the window resized, the capture item changes size; re-fit the pool.
|
||||||
const winrt::SizeInt32 size = item_.Size();
|
const winrt::SizeInt32 size = item_.Size();
|
||||||
if (size.Width != pool_size_.Width || size.Height != pool_size_.Height)
|
if (size.Width != pool_size_.Width || size.Height != pool_size_.Height) {
|
||||||
{
|
|
||||||
pool_size_ = size;
|
pool_size_ = size;
|
||||||
frame_pool_.Recreate(winrt_device_, kPixelFormat, 2, size);
|
frame_pool_.Recreate(winrt_device_, kPixelFormat, 2, size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (latest_srv_ != nullptr)
|
if (latest_srv_ != nullptr) {
|
||||||
{
|
|
||||||
renderer.draw(ctx, latest_srv_.Get(), width_, height_, dst_w, dst_h);
|
renderer.draw(ctx, latest_srv_.Get(), width_, height_, dst_w, dst_h);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,11 @@
|
|||||||
#include <winrt/Windows.Graphics.Capture.h>
|
#include <winrt/Windows.Graphics.Capture.h>
|
||||||
#include <winrt/Windows.Graphics.DirectX.Direct3D11.h>
|
#include <winrt/Windows.Graphics.DirectX.Direct3D11.h>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
class FrameRenderer;
|
class FrameRenderer;
|
||||||
|
|
||||||
class WindowCapture
|
class WindowCapture {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
~WindowCapture();
|
~WindowCapture();
|
||||||
|
|
||||||
@@ -29,28 +27,13 @@ public:
|
|||||||
bool start(HWND target, ID3D11Device* device);
|
bool start(HWND target, ID3D11Device* device);
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
[[nodiscard]] bool running() const
|
[[nodiscard]] bool running() const { return session_ != nullptr; }
|
||||||
{
|
[[nodiscard]] HWND target() const { return target_; }
|
||||||
return session_ != nullptr;
|
[[nodiscard]] std::uint32_t frame_width() const { return width_; }
|
||||||
}
|
[[nodiscard]] std::uint32_t frame_height() const { return height_; }
|
||||||
[[nodiscard]] HWND target() const
|
|
||||||
{
|
|
||||||
return target_;
|
|
||||||
}
|
|
||||||
[[nodiscard]] std::uint32_t frame_width() const
|
|
||||||
{
|
|
||||||
return width_;
|
|
||||||
}
|
|
||||||
[[nodiscard]] std::uint32_t frame_height() const
|
|
||||||
{
|
|
||||||
return height_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cumulative frames WGC has delivered (for the capture-rate metric).
|
// Cumulative frames WGC has delivered (for the capture-rate metric).
|
||||||
[[nodiscard]] std::uint64_t frames_arrived() const
|
[[nodiscard]] std::uint64_t frames_arrived() const { return frames_arrived_; }
|
||||||
{
|
|
||||||
return frames_arrived_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render thread: consume the newest frame (if any) and draw it letterboxed
|
// Render thread: consume the newest frame (if any) and draw it letterboxed
|
||||||
// into a dst_w x dst_h target via `renderer`.
|
// into a dst_w x dst_h target via `renderer`.
|
||||||
|
|||||||
@@ -4,17 +4,14 @@
|
|||||||
#include "injection_panel.hpp"
|
#include "injection_panel.hpp"
|
||||||
#include "ui/app_chrome.hpp"
|
#include "ui/app_chrome.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f);
|
const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f);
|
||||||
const ImVec4 kRed(1.0f, 0.45f, 0.4f, 1.0f);
|
const ImVec4 kRed(1.0f, 0.45f, 0.4f, 1.0f);
|
||||||
|
|
||||||
// One colored line for the multi-series perf graph.
|
// One colored line for the multi-series perf graph.
|
||||||
struct GraphSeries
|
struct GraphSeries {
|
||||||
{
|
|
||||||
const char* name;
|
const char* name;
|
||||||
const float* values; // oldest -> newest
|
const float* values; // oldest -> newest
|
||||||
int count;
|
int count;
|
||||||
@@ -37,16 +34,13 @@ void plot_multiseries(const char* id, const GraphSeries* series, int n_series, f
|
|||||||
|
|
||||||
const float range = (y_max > y_min) ? (y_max - y_min) : 1.0f;
|
const float range = (y_max > y_min) ? (y_max - y_min) : 1.0f;
|
||||||
ImVec2 pts[256];
|
ImVec2 pts[256];
|
||||||
for (int s = 0; s < n_series; ++s)
|
for (int s = 0; s < n_series; ++s) {
|
||||||
{
|
|
||||||
const GraphSeries& g = series[s];
|
const GraphSeries& g = series[s];
|
||||||
if (g.count < 2)
|
if (g.count < 2) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int cnt = g.count > 256 ? 256 : g.count;
|
int cnt = g.count > 256 ? 256 : g.count;
|
||||||
for (int i = 0; i < cnt; ++i)
|
for (int i = 0; i < cnt; ++i) {
|
||||||
{
|
|
||||||
const float t = static_cast<float>(i) / static_cast<float>(cnt - 1);
|
const float t = static_cast<float>(i) / static_cast<float>(cnt - 1);
|
||||||
float norm = (g.values[i] - y_min) / range;
|
float norm = (g.values[i] - y_min) / range;
|
||||||
norm = norm < 0.0f ? 0.0f : (norm > 1.0f ? 1.0f : norm);
|
norm = norm < 0.0f ? 0.0f : (norm > 1.0f ? 1.0f : norm);
|
||||||
@@ -87,12 +81,10 @@ void CapturePanel::draw_ui(const FrameStats& stats)
|
|||||||
const bool have_source = source_ == Source_Hooked ? have_hook : have_wgc_target;
|
const bool have_source = source_ == Source_Hooked ? have_hook : have_wgc_target;
|
||||||
|
|
||||||
ImGui::BeginDisabled(!have_source);
|
ImGui::BeginDisabled(!have_source);
|
||||||
if (ImGui::Checkbox("Mirror game window", &enabled_) && !enabled_)
|
if (ImGui::Checkbox("Mirror game window", &enabled_) && !enabled_) {
|
||||||
{
|
|
||||||
capture_.stop();
|
capture_.stop();
|
||||||
shared_.reset();
|
shared_.reset();
|
||||||
if (injection_ != nullptr)
|
if (injection_ != nullptr) {
|
||||||
{
|
|
||||||
injection_->request_video(false); // stop the in-game Present hook
|
injection_->request_video(false); // stop the in-game Present hook
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,12 +98,10 @@ void CapturePanel::draw_ui(const FrameStats& stats)
|
|||||||
ImGui::RadioButton("WGC", &source_, Source_Wgc);
|
ImGui::RadioButton("WGC", &source_, Source_Wgc);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::RadioButton("Hooked (Present)", &source_, Source_Hooked);
|
ImGui::RadioButton("Hooked (Present)", &source_, Source_Hooked);
|
||||||
if (source_ != prev_source)
|
if (source_ != prev_source) {
|
||||||
{
|
|
||||||
capture_.stop();
|
capture_.stop();
|
||||||
shared_.reset();
|
shared_.reset();
|
||||||
if (injection_ != nullptr)
|
if (injection_ != nullptr) {
|
||||||
{
|
|
||||||
injection_->request_video(enabled_ && source_ == Source_Hooked);
|
injection_->request_video(enabled_ && source_ == Source_Hooked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,61 +113,44 @@ void CapturePanel::draw_ui(const FrameStats& stats)
|
|||||||
ImGui::BeginDisabled(source_ != Source_Hooked || !enabled_);
|
ImGui::BeginDisabled(source_ != Source_Hooked || !enabled_);
|
||||||
ImGui::Checkbox("Sync flip to game frames", &frame_sync_);
|
ImGui::Checkbox("Sync flip to game frames", &frame_sync_);
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
if (source_ != Source_Hooked)
|
if (source_ != Source_Hooked) {
|
||||||
{
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextDisabled("(Hooked only)");
|
ImGui::TextDisabled("(Hooked only)");
|
||||||
}
|
} else if (ImGui::IsItemHovered()) {
|
||||||
else if (ImGui::IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui::SetTooltip("Present in lockstep with the game instead of vsync.");
|
ImGui::SetTooltip("Present in lockstep with the game instead of vsync.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!have_source)
|
if (!have_source) {
|
||||||
{
|
ImGui::TextDisabled(source_ == Source_Hooked ? "Inject into a game first (the Present hook is the source)."
|
||||||
ImGui::TextDisabled(source_ == Source_Hooked
|
|
||||||
? "Inject into a game first (the Present hook is the source)."
|
|
||||||
: "Inject into a game first (its window is the source).");
|
: "Inject into a game first (its window is the source).");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source_ == Source_Wgc)
|
if (source_ == Source_Wgc) {
|
||||||
{
|
|
||||||
// Start/restart WGC capture when enabled and the target window changes.
|
// Start/restart WGC capture when enabled and the target window changes.
|
||||||
if (enabled_ && have_wgc_target && capture_.target() != target_)
|
if (enabled_ && have_wgc_target && capture_.target() != target_) {
|
||||||
{
|
if (!capture_.start(target_, device_)) {
|
||||||
if (!capture_.start(target_, device_))
|
|
||||||
{
|
|
||||||
enabled_ = false;
|
enabled_ = false;
|
||||||
ImGui::TextColored(kRed, "Failed to start capture.");
|
ImGui::TextColored(kRed, "Failed to start capture.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (capture_.running())
|
if (capture_.running()) {
|
||||||
{
|
|
||||||
ImGui::TextColored(kGreen, "Capturing %ux%u (WGC)", capture_.frame_width(), capture_.frame_height());
|
ImGui::TextColored(kGreen, "Capturing %ux%u (WGC)", capture_.frame_width(), capture_.frame_height());
|
||||||
}
|
}
|
||||||
}
|
} else // Source_Hooked
|
||||||
else // Source_Hooked
|
|
||||||
{
|
|
||||||
if (enabled_ && injection_ != nullptr)
|
|
||||||
{
|
{
|
||||||
|
if (enabled_ && injection_ != nullptr) {
|
||||||
// Keep the subsystem requested (a fresh inject may have reset control).
|
// Keep the subsystem requested (a fresh inject may have reset control).
|
||||||
if (!injection_->video_requested())
|
if (!injection_->video_requested()) {
|
||||||
{
|
|
||||||
injection_->request_video(true);
|
injection_->request_video(true);
|
||||||
}
|
}
|
||||||
const VideoShareView share = injection_->video_share();
|
const VideoShareView share = injection_->video_share();
|
||||||
if (shared_.frames_copied() > 0 && shared_.width() > 0)
|
if (shared_.frames_copied() > 0 && shared_.width() > 0) {
|
||||||
{
|
ImGui::TextColored(kGreen, "Mirroring %ux%u (hooked, %llu frames)", shared_.width(), shared_.height(),
|
||||||
ImGui::TextColored(kGreen, "Mirroring %ux%u (hooked, %llu frames)", shared_.width(),
|
static_cast<unsigned long long>(shared_.frames_copied()));
|
||||||
shared_.height(), static_cast<unsigned long long>(shared_.frames_copied()));
|
} else if (share.present_calls > 0) {
|
||||||
}
|
|
||||||
else if (share.present_calls > 0)
|
|
||||||
{
|
|
||||||
ImGui::TextColored(kGreen, "Present hooked (%llu calls); opening shared texture...",
|
ImGui::TextColored(kGreen, "Present hooked (%llu calls); opening shared texture...",
|
||||||
static_cast<unsigned long long>(share.present_calls));
|
static_cast<unsigned long long>(share.present_calls));
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextDisabled("Waiting for hooked frames (the game may not render via DXGI).");
|
ImGui::TextDisabled("Waiting for hooked frames (the game may not render via DXGI).");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,8 +165,7 @@ void CapturePanel::draw_ui(const FrameStats& stats)
|
|||||||
|
|
||||||
void CapturePanel::draw_pipeline_metrics(const FrameStats& stats)
|
void CapturePanel::draw_pipeline_metrics(const FrameStats& stats)
|
||||||
{
|
{
|
||||||
if (!enabled_)
|
if (!enabled_) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const double now = ImGui::GetTime();
|
const double now = ImGui::GetTime();
|
||||||
@@ -202,8 +174,7 @@ void CapturePanel::draw_pipeline_metrics(const FrameStats& stats)
|
|||||||
// thresholds (e.g. 99 -> 100) frame to frame.
|
// thresholds (e.g. 99 -> 100) frame to frame.
|
||||||
ImGui::Text("Tool render: %4.0f FPS (%6.2f ms)", stats.fps(), stats.avg_ms());
|
ImGui::Text("Tool render: %4.0f FPS (%6.2f ms)", stats.fps(), stats.avg_ms());
|
||||||
|
|
||||||
if (source_ == Source_Hooked)
|
if (source_ == Source_Hooked) {
|
||||||
{
|
|
||||||
const VideoShareView v = injection_ != nullptr ? injection_->video_share() : VideoShareView{};
|
const VideoShareView v = injection_ != nullptr ? injection_->video_share() : VideoShareView{};
|
||||||
ImGui::Text("Game present: %5.0f /s", present_rate_.sample(v.present_calls, now));
|
ImGui::Text("Game present: %5.0f /s", present_rate_.sample(v.present_calls, now));
|
||||||
ImGui::Text("Hook publish: %5.0f /s", capture_rate_.sample(v.generation, now));
|
ImGui::Text("Hook publish: %5.0f /s", capture_rate_.sample(v.generation, now));
|
||||||
@@ -218,8 +189,7 @@ void CapturePanel::draw_pipeline_metrics(const FrameStats& stats)
|
|||||||
disp_skip);
|
disp_skip);
|
||||||
|
|
||||||
// On each newly published frame, measure now - present_qpc (system-wide clock).
|
// On each newly published frame, measure now - present_qpc (system-wide clock).
|
||||||
if (v.generation != last_video_gen_ && v.present_qpc != 0 && qpc_freq_ > 0)
|
if (v.generation != last_video_gen_ && v.present_qpc != 0 && qpc_freq_ > 0) {
|
||||||
{
|
|
||||||
last_video_gen_ = v.generation;
|
last_video_gen_ = v.generation;
|
||||||
LARGE_INTEGER now_qpc{};
|
LARGE_INTEGER now_qpc{};
|
||||||
QueryPerformanceCounter(&now_qpc);
|
QueryPerformanceCounter(&now_qpc);
|
||||||
@@ -228,12 +198,10 @@ void CapturePanel::draw_pipeline_metrics(const FrameStats& stats)
|
|||||||
if (ms >= 0.0 && ms < 1000.0) // ignore clock edge cases
|
if (ms >= 0.0 && ms < 1000.0) // ignore clock edge cases
|
||||||
{
|
{
|
||||||
lat_sum_ += ms;
|
lat_sum_ += ms;
|
||||||
if (lat_n_ == 0 || ms < lat_wmin_)
|
if (lat_n_ == 0 || ms < lat_wmin_) {
|
||||||
{
|
|
||||||
lat_wmin_ = ms;
|
lat_wmin_ = ms;
|
||||||
}
|
}
|
||||||
if (ms > lat_wmax_)
|
if (ms > lat_wmax_) {
|
||||||
{
|
|
||||||
lat_wmax_ = ms;
|
lat_wmax_ = ms;
|
||||||
}
|
}
|
||||||
++lat_n_;
|
++lat_n_;
|
||||||
@@ -241,8 +209,7 @@ void CapturePanel::draw_pipeline_metrics(const FrameStats& stats)
|
|||||||
}
|
}
|
||||||
if (now - lat_window_start_ >= 1.0) // publish min/avg/max once a second
|
if (now - lat_window_start_ >= 1.0) // publish min/avg/max once a second
|
||||||
{
|
{
|
||||||
if (lat_n_ > 0)
|
if (lat_n_ > 0) {
|
||||||
{
|
|
||||||
lat_avg_ = static_cast<float>(lat_sum_ / lat_n_);
|
lat_avg_ = static_cast<float>(lat_sum_ / lat_n_);
|
||||||
lat_min_ = static_cast<float>(lat_wmin_);
|
lat_min_ = static_cast<float>(lat_wmin_);
|
||||||
lat_max_ = static_cast<float>(lat_wmax_);
|
lat_max_ = static_cast<float>(lat_wmax_);
|
||||||
@@ -253,17 +220,12 @@ void CapturePanel::draw_pipeline_metrics(const FrameStats& stats)
|
|||||||
lat_wmax_ = 0.0;
|
lat_wmax_ = 0.0;
|
||||||
lat_window_start_ = now;
|
lat_window_start_ = now;
|
||||||
}
|
}
|
||||||
if (lat_avg_ > 0.0f)
|
if (lat_avg_ > 0.0f) {
|
||||||
{
|
|
||||||
ImGui::Text("Capture->display: avg %6.1f min %6.1f max %6.1f ms", lat_avg_, lat_min_, lat_max_);
|
ImGui::Text("Capture->display: avg %6.1f min %6.1f max %6.1f ms", lat_avg_, lat_min_, lat_max_);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextDisabled("Capture->display latency: measuring...");
|
ImGui::TextDisabled("Capture->display latency: measuring...");
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextDisabled("Game present: n/a (WGC has no game frame timing)");
|
ImGui::TextDisabled("Game present: n/a (WGC has no game frame timing)");
|
||||||
ImGui::Text("WGC capture: %5.0f /s", capture_rate_.sample(capture_.frames_arrived(), now));
|
ImGui::Text("WGC capture: %5.0f /s", capture_rate_.sample(capture_.frames_arrived(), now));
|
||||||
ImGui::TextDisabled("Latency: n/a (WGC frames aren't game-timestamped)");
|
ImGui::TextDisabled("Latency: n/a (WGC frames aren't game-timestamped)");
|
||||||
@@ -277,14 +239,11 @@ void CapturePanel::sample_graph_series(double now)
|
|||||||
const float dt = ImGui::GetIO().DeltaTime;
|
const float dt = ImGui::GetIO().DeltaTime;
|
||||||
tool_fps_.push(dt > 0.0f ? 1.0f / dt : 0.0f);
|
tool_fps_.push(dt > 0.0f ? 1.0f / dt : 0.0f);
|
||||||
|
|
||||||
if (source_ == Source_Hooked)
|
if (source_ == Source_Hooked) {
|
||||||
{
|
|
||||||
const VideoShareView v = injection_ != nullptr ? injection_->video_share() : VideoShareView{};
|
const VideoShareView v = injection_ != nullptr ? injection_->video_share() : VideoShareView{};
|
||||||
game_fps_.push(game_edge_.sample(v.present_calls, now));
|
game_fps_.push(game_edge_.sample(v.present_calls, now));
|
||||||
hook_fps_.push(hook_edge_.sample(v.generation, now));
|
hook_fps_.push(hook_edge_.sample(v.generation, now));
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
wgc_fps_.push(wgc_edge_.sample(capture_.frames_arrived(), now));
|
wgc_fps_.push(wgc_edge_.sample(capture_.frames_arrived(), now));
|
||||||
}
|
}
|
||||||
last_graph_time_ = now;
|
last_graph_time_ = now;
|
||||||
@@ -310,8 +269,7 @@ void CapturePanel::draw_perf_graphs(const FrameStats& /*stats*/)
|
|||||||
|
|
||||||
const auto add = [&](const Series& s, const char* name, const ImVec4& col) {
|
const auto add = [&](const Series& s, const char* name, const ImVec4& col) {
|
||||||
const int c = s.copy(fps[n]);
|
const int c = s.copy(fps[n]);
|
||||||
for (int i = 0; i < c; ++i)
|
for (int i = 0; i < c; ++i) {
|
||||||
{
|
|
||||||
ms[n][i] = fps[n][i] > 1.0f ? 1000.0f / fps[n][i] : 0.0f;
|
ms[n][i] = fps[n][i] > 1.0f ? 1000.0f / fps[n][i] : 0.0f;
|
||||||
}
|
}
|
||||||
names[n] = name;
|
names[n] = name;
|
||||||
@@ -321,42 +279,34 @@ void CapturePanel::draw_perf_graphs(const FrameStats& /*stats*/)
|
|||||||
};
|
};
|
||||||
|
|
||||||
add(tool_fps_, "Tool", col_tool);
|
add(tool_fps_, "Tool", col_tool);
|
||||||
if (source_ == Source_Hooked)
|
if (source_ == Source_Hooked) {
|
||||||
{
|
|
||||||
add(game_fps_, "Game", col_game);
|
add(game_fps_, "Game", col_game);
|
||||||
add(hook_fps_, "Hook", col_hook);
|
add(hook_fps_, "Hook", col_hook);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
add(wgc_fps_, "WGC", col_hook);
|
add(wgc_fps_, "WGC", col_hook);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (counts[0] < 2)
|
if (counts[0] < 2) {
|
||||||
{
|
|
||||||
ImGui::TextDisabled("Gathering samples...");
|
ImGui::TextDisabled("Gathering samples...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legend: a colored label + the latest value of each line, so colors map to series.
|
// Legend: a colored label + the latest value of each line, so colors map to series.
|
||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < n; ++i) {
|
||||||
{
|
|
||||||
ImGui::TextColored(cols[i], "%-4s %4.0f", names[i], counts[i] > 0 ? fps[i][counts[i] - 1] : 0.0f);
|
ImGui::TextColored(cols[i], "%-4s %4.0f", names[i], counts[i] > 0 ? fps[i][counts[i] - 1] : 0.0f);
|
||||||
if (i + 1 < n)
|
if (i + 1 < n) {
|
||||||
{
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GraphSeries gs[3];
|
GraphSeries gs[3];
|
||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < n; ++i) {
|
||||||
{
|
|
||||||
gs[i] = GraphSeries{names[i], fps[i], counts[i], cols[i]};
|
gs[i] = GraphSeries{names[i], fps[i], counts[i], cols[i]};
|
||||||
}
|
}
|
||||||
ImGui::TextDisabled("FPS (0-144)");
|
ImGui::TextDisabled("FPS (0-144)");
|
||||||
plot_multiseries("##fps_multi", gs, n, 0.0f, 144.0f, 56.0f);
|
plot_multiseries("##fps_multi", gs, n, 0.0f, 144.0f, 56.0f);
|
||||||
|
|
||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < n; ++i) {
|
||||||
{
|
|
||||||
gs[i].values = ms[i];
|
gs[i].values = ms[i];
|
||||||
}
|
}
|
||||||
ImGui::TextDisabled("Frametime (0-33 ms)");
|
ImGui::TextDisabled("Frametime (0-33 ms)");
|
||||||
@@ -365,23 +315,17 @@ void CapturePanel::draw_perf_graphs(const FrameStats& /*stats*/)
|
|||||||
|
|
||||||
void CapturePanel::render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h)
|
void CapturePanel::render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h)
|
||||||
{
|
{
|
||||||
if (!enabled_)
|
if (!enabled_) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (source_ == Source_Hooked)
|
if (source_ == Source_Hooked) {
|
||||||
{
|
if (injection_ == nullptr) {
|
||||||
if (injection_ == nullptr)
|
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (shared_.update(injection_->video_share(), injection_->target_pid()) && shared_.srv() != nullptr)
|
if (shared_.update(injection_->video_share(), injection_->target_pid()) && shared_.srv() != nullptr) {
|
||||||
{
|
|
||||||
renderer_.draw(ctx, shared_.srv(), shared_.width(), shared_.height(), dst_w, dst_h);
|
renderer_.draw(ctx, shared_.srv(), shared_.width(), shared_.height(), dst_w, dst_h);
|
||||||
}
|
}
|
||||||
}
|
} else if (capture_.running()) {
|
||||||
else if (capture_.running())
|
|
||||||
{
|
|
||||||
capture_.draw_latest(renderer_, ctx, dst_w, dst_h);
|
capture_.draw_latest(renderer_, ctx, dst_w, dst_h);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,28 +14,20 @@
|
|||||||
#include "capture/window_capture.hpp"
|
#include "capture/window_capture.hpp"
|
||||||
#include "ui/app_chrome.hpp"
|
#include "ui/app_chrome.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
class InjectionPanel;
|
class InjectionPanel;
|
||||||
|
|
||||||
class CapturePanel
|
class CapturePanel {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
bool init(ID3D11Device* device);
|
bool init(ID3D11Device* device);
|
||||||
|
|
||||||
// The window to mirror via WGC (0 if none yet); typically the injected game's HWND.
|
// The window to mirror via WGC (0 if none yet); typically the injected game's HWND.
|
||||||
void set_target(HWND target)
|
void set_target(HWND target) { target_ = target; }
|
||||||
{
|
|
||||||
target_ = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The injection panel supplies the target pid + Present-hook video channel and
|
// The injection panel supplies the target pid + Present-hook video channel and
|
||||||
// lets this panel install/remove the video subsystem when the source is Hooked.
|
// lets this panel install/remove the video subsystem when the source is Hooked.
|
||||||
void set_injection(InjectionPanel* injection)
|
void set_injection(InjectionPanel* injection) { injection_ = injection; }
|
||||||
{
|
|
||||||
injection_ = injection;
|
|
||||||
}
|
|
||||||
|
|
||||||
// `stats` are the host's render frame-timing, drawn as the mirror's
|
// `stats` are the host's render frame-timing, drawn as the mirror's
|
||||||
// frametime / FPS graphs (this window is what the mirror renders into).
|
// frametime / FPS graphs (this window is what the mirror renders into).
|
||||||
@@ -46,17 +38,11 @@ public:
|
|||||||
|
|
||||||
// Whether the video mirror is on (mouse forwarding is gated on this, since the
|
// Whether the video mirror is on (mouse forwarding is gated on this, since the
|
||||||
// operator can't aim clicks without seeing the game).
|
// operator can't aim clicks without seeing the game).
|
||||||
[[nodiscard]] bool mirroring() const
|
[[nodiscard]] bool mirroring() const { return enabled_; }
|
||||||
{
|
|
||||||
return enabled_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// True when the active source is the injected Present-hook (client/backbuffer);
|
// True when the active source is the injected Present-hook (client/backbuffer);
|
||||||
// false for WGC (whole-window). Drives the mouse coordinate mapping.
|
// false for WGC (whole-window). Drives the mouse coordinate mapping.
|
||||||
[[nodiscard]] bool source_hooked() const
|
[[nodiscard]] bool source_hooked() const { return source_ == Source_Hooked; }
|
||||||
{
|
|
||||||
return source_ == Source_Hooked;
|
|
||||||
}
|
|
||||||
|
|
||||||
// True when the operator asked to pace the tool's flip to the game's published frames
|
// True when the operator asked to pace the tool's flip to the game's published frames
|
||||||
// (only meaningful with the Hooked source while mirroring) AND the target is alive and
|
// (only meaningful with the Hooked source while mirroring) AND the target is alive and
|
||||||
@@ -67,8 +53,7 @@ public:
|
|||||||
[[nodiscard]] bool frame_sync_active() const;
|
[[nodiscard]] bool frame_sync_active() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum Source : int
|
enum Source : int {
|
||||||
{
|
|
||||||
Source_Wgc = 0, // Windows Graphics Capture
|
Source_Wgc = 0, // Windows Graphics Capture
|
||||||
Source_Hooked = 1, // injected Present-hook shared texture
|
Source_Hooked = 1, // injected Present-hook shared texture
|
||||||
};
|
};
|
||||||
@@ -77,15 +62,13 @@ private:
|
|||||||
void draw_pipeline_metrics(const FrameStats& stats);
|
void draw_pipeline_metrics(const FrameStats& stats);
|
||||||
|
|
||||||
// Turns a monotonic counter into a rate (recomputed ~2x/second).
|
// Turns a monotonic counter into a rate (recomputed ~2x/second).
|
||||||
struct RateTracker
|
struct RateTracker {
|
||||||
{
|
|
||||||
std::uint64_t last_count = 0;
|
std::uint64_t last_count = 0;
|
||||||
double last_time = 0.0;
|
double last_time = 0.0;
|
||||||
double rate = 0.0;
|
double rate = 0.0;
|
||||||
double sample(std::uint64_t count, double now)
|
double sample(std::uint64_t count, double now)
|
||||||
{
|
{
|
||||||
if (now - last_time >= 0.5)
|
if (now - last_time >= 0.5) {
|
||||||
{
|
|
||||||
const double dt = now - last_time;
|
const double dt = now - last_time;
|
||||||
rate = dt > 0.0 ? static_cast<double>(count - last_count) / dt : 0.0;
|
rate = dt > 0.0 ? static_cast<double>(count - last_count) / dt : 0.0;
|
||||||
last_count = count;
|
last_count = count;
|
||||||
@@ -98,25 +81,20 @@ private:
|
|||||||
// Turns a monotonic counter into an instantaneous rate the moment it advances (so a
|
// Turns a monotonic counter into an instantaneous rate the moment it advances (so a
|
||||||
// per-frame graph has real resolution instead of 0.5 s stair-steps); the rate is
|
// per-frame graph has real resolution instead of 0.5 s stair-steps); the rate is
|
||||||
// held between advances. Used for the game-present / hook-publish graph series.
|
// held between advances. Used for the game-present / hook-publish graph series.
|
||||||
struct EdgeRate
|
struct EdgeRate {
|
||||||
{
|
|
||||||
std::uint64_t last_count = 0;
|
std::uint64_t last_count = 0;
|
||||||
double last_time = 0.0;
|
double last_time = 0.0;
|
||||||
float fps = 0.0f;
|
float fps = 0.0f;
|
||||||
bool primed = false;
|
bool primed = false;
|
||||||
float sample(std::uint64_t count, double now)
|
float sample(std::uint64_t count, double now)
|
||||||
{
|
{
|
||||||
if (!primed)
|
if (!primed) {
|
||||||
{
|
|
||||||
last_count = count;
|
last_count = count;
|
||||||
last_time = now;
|
last_time = now;
|
||||||
primed = true;
|
primed = true;
|
||||||
}
|
} else if (count != last_count) {
|
||||||
else if (count != last_count)
|
|
||||||
{
|
|
||||||
const double dt = now - last_time;
|
const double dt = now - last_time;
|
||||||
if (dt > 0.0)
|
if (dt > 0.0) {
|
||||||
{
|
|
||||||
fps = static_cast<float>(static_cast<double>(count - last_count) / dt);
|
fps = static_cast<float>(static_cast<double>(count - last_count) / dt);
|
||||||
}
|
}
|
||||||
last_count = count;
|
last_count = count;
|
||||||
@@ -127,8 +105,7 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Fixed-length rolling history of one FPS series, plotted in the perf graph.
|
// Fixed-length rolling history of one FPS series, plotted in the perf graph.
|
||||||
struct Series
|
struct Series {
|
||||||
{
|
|
||||||
static constexpr int kCap = 240; // ~2 s at 120 FPS, matches FrameStats
|
static constexpr int kCap = 240; // ~2 s at 120 FPS, matches FrameStats
|
||||||
float v[kCap] = {};
|
float v[kCap] = {};
|
||||||
int pos = 0;
|
int pos = 0;
|
||||||
@@ -137,8 +114,7 @@ private:
|
|||||||
{
|
{
|
||||||
v[pos] = fps;
|
v[pos] = fps;
|
||||||
pos = (pos + 1) % kCap;
|
pos = (pos + 1) % kCap;
|
||||||
if (count < kCap)
|
if (count < kCap) {
|
||||||
{
|
|
||||||
++count;
|
++count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,16 +122,12 @@ private:
|
|||||||
int copy(float* out) const
|
int copy(float* out) const
|
||||||
{
|
{
|
||||||
const int start = (pos - count + kCap * 2) % kCap;
|
const int start = (pos - count + kCap * 2) % kCap;
|
||||||
for (int i = 0; i < count; ++i)
|
for (int i = 0; i < count; ++i) {
|
||||||
{
|
|
||||||
out[i] = v[(start + i) % kCap];
|
out[i] = v[(start + i) % kCap];
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
float latest() const
|
float latest() const { return count > 0 ? v[(pos - 1 + kCap) % kCap] : 0.0f; }
|
||||||
{
|
|
||||||
return count > 0 ? v[(pos - 1 + kCap) % kCap] : 0.0f;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Push one sample into each graph series for the current frame/source.
|
// Push one sample into each graph series for the current frame/source.
|
||||||
|
|||||||
@@ -4,17 +4,14 @@
|
|||||||
|
|
||||||
#include "ui/app_chrome.hpp"
|
#include "ui/app_chrome.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f);
|
const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f);
|
||||||
const ImVec4 kGrey(0.7f, 0.7f, 0.7f, 1.0f);
|
const ImVec4 kGrey(0.7f, 0.7f, 0.7f, 1.0f);
|
||||||
|
|
||||||
struct ButtonBit
|
struct ButtonBit {
|
||||||
{
|
|
||||||
std::uint16_t mask;
|
std::uint16_t mask;
|
||||||
const char* label;
|
const char* label;
|
||||||
};
|
};
|
||||||
@@ -29,16 +26,14 @@ constexpr ButtonBit kButtons[] = {
|
|||||||
void draw_pad(int index, const PadInfo& pad, bool debug_details)
|
void draw_pad(int index, const PadInfo& pad, bool debug_details)
|
||||||
{
|
{
|
||||||
ImGui::PushID(index);
|
ImGui::PushID(index);
|
||||||
if (!pad.connected)
|
if (!pad.connected) {
|
||||||
{
|
|
||||||
ImGui::TextDisabled("Slot %d: disconnected", index);
|
ImGui::TextDisabled("Slot %d: disconnected", index);
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TextColored(kGreen, "Slot %d [%s]", index, pad.source.c_str());
|
ImGui::TextColored(kGreen, "Slot %d [%s]", index, pad.source.c_str());
|
||||||
if (debug_details)
|
if (debug_details) {
|
||||||
{
|
|
||||||
// Triggers on the slot line (saves a row); thumbsticks below.
|
// Triggers on the slot line (saves a row); thumbsticks below.
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextDisabled("LT %3u RT %3u", pad.state.left_trigger, pad.state.right_trigger);
|
ImGui::TextDisabled("LT %3u RT %3u", pad.state.left_trigger, pad.state.right_trigger);
|
||||||
@@ -46,25 +41,21 @@ void draw_pad(int index, const PadInfo& pad, bool debug_details)
|
|||||||
|
|
||||||
bool first = true;
|
bool first = true;
|
||||||
ImGui::TextUnformatted("Buttons: ");
|
ImGui::TextUnformatted("Buttons: ");
|
||||||
for (const ButtonBit& b : kButtons)
|
for (const ButtonBit& b : kButtons) {
|
||||||
{
|
if ((pad.state.buttons & b.mask) != 0) {
|
||||||
if ((pad.state.buttons & b.mask) != 0)
|
|
||||||
{
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextColored(kGreen, "%s%s", first ? "" : ", ", b.label);
|
ImGui::TextColored(kGreen, "%s%s", first ? "" : ", ", b.label);
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (first)
|
if (first) {
|
||||||
{
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextDisabled("(none)");
|
ImGui::TextDisabled("(none)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debug_details)
|
if (debug_details) {
|
||||||
{
|
ImGui::Text("L (%6d, %6d) R (%6d, %6d)", pad.state.thumb_lx, pad.state.thumb_ly, pad.state.thumb_rx,
|
||||||
ImGui::Text("L (%6d, %6d) R (%6d, %6d)", pad.state.thumb_lx, pad.state.thumb_ly,
|
pad.state.thumb_ry);
|
||||||
pad.state.thumb_rx, pad.state.thumb_ry);
|
|
||||||
}
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
@@ -81,15 +72,13 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st
|
|||||||
|
|
||||||
#ifdef COOP_WITH_STEAM
|
#ifdef COOP_WITH_STEAM
|
||||||
ImGui::Checkbox("Use Steam Input (experimental)", &steam_requested_);
|
ImGui::Checkbox("Use Steam Input (experimental)", &steam_requested_);
|
||||||
if (steam_requested_)
|
if (steam_requested_) {
|
||||||
{
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextColored(steam_active_ ? kGreen : kGrey, steam_active_ ? "(active)" : "(starting...)");
|
ImGui::TextColored(steam_active_ ? kGreen : kGrey, steam_active_ ? "(active)" : "(starting...)");
|
||||||
ImGui::TextDisabled("Needs a controller bound to Steam Input for this app; otherwise");
|
ImGui::TextDisabled("Needs a controller bound to Steam Input for this app; otherwise");
|
||||||
ImGui::TextDisabled("XInput is hidden and no input arrives. Leave off for plain XInput.");
|
ImGui::TextDisabled("XInput is hidden and no input arrives. Leave off for plain XInput.");
|
||||||
}
|
}
|
||||||
if (steam_note_[0] != '\0')
|
if (steam_note_[0] != '\0') {
|
||||||
{
|
|
||||||
ImGui::TextColored(kGrey, "%s", steam_note_);
|
ImGui::TextColored(kGrey, "%s", steam_note_);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -97,13 +86,11 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st
|
|||||||
// Synthetic test input (debug aid): drives the game with a non-human pattern so
|
// Synthetic test input (debug aid): drives the game with a non-human pattern so
|
||||||
// forwarding can be proven without a real controller. Only meaningful once the
|
// forwarding can be proven without a real controller. Only meaningful once the
|
||||||
// XInput hook is attached.
|
// XInput hook is attached.
|
||||||
if (debug_details)
|
if (debug_details) {
|
||||||
{
|
|
||||||
ImGui::BeginDisabled(!status.attached);
|
ImGui::BeginDisabled(!status.attached);
|
||||||
ImGui::Checkbox("Forward synthetic test input", &test_input_);
|
ImGui::Checkbox("Forward synthetic test input", &test_input_);
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
if (test_input_)
|
if (test_input_) {
|
||||||
{
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextDisabled("(ignores your controller)");
|
ImGui::TextDisabled("(ignores your controller)");
|
||||||
}
|
}
|
||||||
@@ -112,15 +99,13 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st
|
|||||||
// --- Guest pads the host receives from RPT -----------------------------
|
// --- Guest pads the host receives from RPT -----------------------------
|
||||||
ImGui::SeparatorText("Incoming (host receives)");
|
ImGui::SeparatorText("Incoming (host receives)");
|
||||||
const auto& pads = input.pads;
|
const auto& pads = input.pads;
|
||||||
for (int i = 0; i < static_cast<int>(pads.size()); ++i)
|
for (int i = 0; i < static_cast<int>(pads.size()); ++i) {
|
||||||
{
|
|
||||||
draw_pad(i, pads[i], debug_details);
|
draw_pad(i, pads[i], debug_details);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- What the injected game reads back via the XInput hook --------------
|
// --- What the injected game reads back via the XInput hook --------------
|
||||||
ImGui::SeparatorText("Game polling (hook reports)");
|
ImGui::SeparatorText("Game polling (hook reports)");
|
||||||
if (!status.attached)
|
if (!status.attached) {
|
||||||
{
|
|
||||||
ImGui::TextDisabled("Not injected (no XInput hook).");
|
ImGui::TextDisabled("Not injected (no XInput hook).");
|
||||||
record_panel_fit("Controllers");
|
record_panel_fit("Controllers");
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
@@ -129,11 +114,9 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st
|
|||||||
|
|
||||||
// Convert the cumulative per-slot counters into rates every half second.
|
// Convert the cumulative per-slot counters into rates every half second.
|
||||||
const double now = ImGui::GetTime();
|
const double now = ImGui::GetTime();
|
||||||
if (now - last_sample_time_ >= 0.5)
|
if (now - last_sample_time_ >= 0.5) {
|
||||||
{
|
|
||||||
const double dt = now - last_sample_time_;
|
const double dt = now - last_sample_time_;
|
||||||
for (int i = 0; i < static_cast<int>(kMaxPads); ++i)
|
for (int i = 0; i < static_cast<int>(kMaxPads); ++i) {
|
||||||
{
|
|
||||||
const unsigned long long delta =
|
const unsigned long long delta =
|
||||||
status.get_state[i] >= last_state_count_[i] ? status.get_state[i] - last_state_count_[i] : 0;
|
status.get_state[i] >= last_state_count_[i] ? status.get_state[i] - last_state_count_[i] : 0;
|
||||||
state_rate_[i] = dt > 0.0 ? static_cast<double>(delta) / dt : 0.0;
|
state_rate_[i] = dt > 0.0 ? static_cast<double>(delta) / dt : 0.0;
|
||||||
@@ -143,16 +126,12 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st
|
|||||||
}
|
}
|
||||||
|
|
||||||
double total_rate = 0.0;
|
double total_rate = 0.0;
|
||||||
for (int i = 0; i < static_cast<int>(kMaxPads); ++i)
|
for (int i = 0; i < static_cast<int>(kMaxPads); ++i) {
|
||||||
{
|
|
||||||
total_rate += state_rate_[i];
|
total_rate += state_rate_[i];
|
||||||
}
|
}
|
||||||
if (total_rate > 0.0)
|
if (total_rate > 0.0) {
|
||||||
{
|
|
||||||
ImGui::TextColored(kGreen, "Game reading controller: %5.0f polls/s", total_rate);
|
ImGui::TextColored(kGreen, "Game reading controller: %5.0f polls/s", total_rate);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextColored(kGrey, "Game reading controller: idle");
|
ImGui::TextColored(kGrey, "Game reading controller: idle");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,9 +139,7 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st
|
|||||||
// (what we forwarded vs what the game read back through the hook). A round-trip mismatch
|
// (what we forwarded vs what the game read back through the hook). A round-trip mismatch
|
||||||
// isolates a tool->game forwarding problem from an input->tool one. Merged into a single
|
// isolates a tool->game forwarding problem from an input->tool one. Merged into a single
|
||||||
// table so the (debug) controller view stays inside its panel even with every slot busy.
|
// table so the (debug) controller view stays inside its panel even with every slot busy.
|
||||||
if (debug_details &&
|
if (debug_details && ImGui::BeginTable("slots", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) {
|
||||||
ImGui::BeginTable("slots", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp))
|
|
||||||
{
|
|
||||||
ImGui::TableSetupColumn("Slot");
|
ImGui::TableSetupColumn("Slot");
|
||||||
ImGui::TableSetupColumn("Poll/s");
|
ImGui::TableSetupColumn("Poll/s");
|
||||||
ImGui::TableSetupColumn("Polls");
|
ImGui::TableSetupColumn("Polls");
|
||||||
@@ -170,20 +147,16 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st
|
|||||||
ImGui::TableSetupColumn("Game read btn/LX,LY");
|
ImGui::TableSetupColumn("Game read btn/LX,LY");
|
||||||
ImGui::TableHeadersRow();
|
ImGui::TableHeadersRow();
|
||||||
const auto& fwd = input.pads;
|
const auto& fwd = input.pads;
|
||||||
for (int i = 0; i < static_cast<int>(kMaxPads); ++i)
|
for (int i = 0; i < static_cast<int>(kMaxPads); ++i) {
|
||||||
{
|
|
||||||
const CoopPadState& f = fwd[i].state;
|
const CoopPadState& f = fwd[i].state;
|
||||||
const CoopPadState& r = status.read_state[i];
|
const CoopPadState& r = status.read_state[i];
|
||||||
ImGui::TableNextRow();
|
ImGui::TableNextRow();
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
ImGui::Text("%d", i);
|
ImGui::Text("%d", i);
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
if (state_rate_[i] > 0.0)
|
if (state_rate_[i] > 0.0) {
|
||||||
{
|
|
||||||
ImGui::TextColored(kGreen, "%5.0f", state_rate_[i]);
|
ImGui::TextColored(kGreen, "%5.0f", state_rate_[i]);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextDisabled("0");
|
ImGui::TextDisabled("0");
|
||||||
}
|
}
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
|
|||||||
@@ -11,11 +11,9 @@
|
|||||||
#include "input/input_source.hpp"
|
#include "input/input_source.hpp"
|
||||||
#include "ipc/ipc_server.hpp"
|
#include "ipc/ipc_server.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
class ControllersPanel
|
class ControllersPanel {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
// `input` is the input worker's latest snapshot (guest pads + active backend);
|
// `input` is the input worker's latest snapshot (guest pads + active backend);
|
||||||
// `status` is the hook's back-channel (per-slot poll counters); `debug_details`
|
// `status` is the hook's back-channel (per-slot poll counters); `debug_details`
|
||||||
@@ -25,10 +23,7 @@ public:
|
|||||||
|
|
||||||
// Whether the operator enabled "Forward synthetic test input" (a controller debug
|
// Whether the operator enabled "Forward synthetic test input" (a controller debug
|
||||||
// aid). The host feeds this to InjectionPanel, which substitutes a synthetic pad.
|
// aid). The host feeds this to InjectionPanel, which substitutes a synthetic pad.
|
||||||
[[nodiscard]] bool test_input() const
|
[[nodiscard]] bool test_input() const { return test_input_; }
|
||||||
{
|
|
||||||
return test_input_;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef COOP_WITH_STEAM
|
#ifdef COOP_WITH_STEAM
|
||||||
// Whether the operator has opted into Steam Input. It's off by default: simply
|
// Whether the operator has opted into Steam Input. It's off by default: simply
|
||||||
@@ -36,14 +31,8 @@ public:
|
|||||||
// which hides controllers from XInput unless they're bound to our action set for
|
// which hides controllers from XInput unless they're bound to our action set for
|
||||||
// this app -- so it can silently break the (working) XInput path. main reconciles
|
// this app -- so it can silently break the (working) XInput path. main reconciles
|
||||||
// this against the actual backend each frame.
|
// this against the actual backend each frame.
|
||||||
[[nodiscard]] bool steam_input_requested() const
|
[[nodiscard]] bool steam_input_requested() const { return steam_requested_; }
|
||||||
{
|
void set_steam_active(bool active) { steam_active_ = active; }
|
||||||
return steam_requested_;
|
|
||||||
}
|
|
||||||
void set_steam_active(bool active)
|
|
||||||
{
|
|
||||||
steam_active_ = active;
|
|
||||||
}
|
|
||||||
void on_steam_init_failed()
|
void on_steam_init_failed()
|
||||||
{
|
{
|
||||||
steam_requested_ = false;
|
steam_requested_ = false;
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg
|
|||||||
|
|
||||||
using Microsoft::WRL::ComPtr;
|
using Microsoft::WRL::ComPtr;
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
constexpr wchar_t kWindowClass[] = L"CoopAllTheThingsWindow";
|
constexpr wchar_t kWindowClass[] = L"CoopAllTheThingsWindow";
|
||||||
|
|
||||||
// Encode a tightly-packed/row-pitched RGBA8 image to a PNG file via WIC. `src` is the
|
// Encode a tightly-packed/row-pitched RGBA8 image to a PNG file via WIC. `src` is the
|
||||||
@@ -24,42 +22,34 @@ bool write_rgba8_png(const std::wstring& path, UINT width, UINT height, const BY
|
|||||||
{
|
{
|
||||||
ComPtr<IWICImagingFactory> factory;
|
ComPtr<IWICImagingFactory> factory;
|
||||||
if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
|
if (FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
|
||||||
IID_PPV_ARGS(factory.GetAddressOf()))))
|
IID_PPV_ARGS(factory.GetAddressOf())))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ComPtr<IWICBitmap> bitmap; // wrap the back-buffer bytes (RGBA, matches the swap chain)
|
ComPtr<IWICBitmap> bitmap; // wrap the back-buffer bytes (RGBA, matches the swap chain)
|
||||||
if (FAILED(factory->CreateBitmapFromMemory(width, height, GUID_WICPixelFormat32bppRGBA, row_pitch,
|
if (FAILED(factory->CreateBitmapFromMemory(width, height, GUID_WICPixelFormat32bppRGBA, row_pitch,
|
||||||
row_pitch * height, const_cast<BYTE*>(src),
|
row_pitch * height, const_cast<BYTE*>(src), bitmap.GetAddressOf()))) {
|
||||||
bitmap.GetAddressOf())))
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ComPtr<IWICStream> stream;
|
ComPtr<IWICStream> stream;
|
||||||
if (FAILED(factory->CreateStream(stream.GetAddressOf())) ||
|
if (FAILED(factory->CreateStream(stream.GetAddressOf()))
|
||||||
FAILED(stream->InitializeFromFilename(path.c_str(), GENERIC_WRITE)))
|
|| FAILED(stream->InitializeFromFilename(path.c_str(), GENERIC_WRITE))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ComPtr<IWICBitmapEncoder> encoder;
|
ComPtr<IWICBitmapEncoder> encoder;
|
||||||
if (FAILED(factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.GetAddressOf())) ||
|
if (FAILED(factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, encoder.GetAddressOf()))
|
||||||
FAILED(encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache)))
|
|| FAILED(encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ComPtr<IWICBitmapFrameEncode> frame;
|
ComPtr<IWICBitmapFrameEncode> frame;
|
||||||
ComPtr<IPropertyBag2> props;
|
ComPtr<IPropertyBag2> props;
|
||||||
if (FAILED(encoder->CreateNewFrame(frame.GetAddressOf(), props.GetAddressOf())) ||
|
if (FAILED(encoder->CreateNewFrame(frame.GetAddressOf(), props.GetAddressOf()))
|
||||||
FAILED(frame->Initialize(props.Get())) || FAILED(frame->SetSize(width, height)))
|
|| FAILED(frame->Initialize(props.Get())) || FAILED(frame->SetSize(width, height))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Let the encoder pick its native pixel format; WriteSource converts our RGBA to it.
|
// Let the encoder pick its native pixel format; WriteSource converts our RGBA to it.
|
||||||
WICPixelFormatGUID fmt = GUID_WICPixelFormat32bppBGRA;
|
WICPixelFormatGUID fmt = GUID_WICPixelFormat32bppBGRA;
|
||||||
frame->SetPixelFormat(&fmt);
|
frame->SetPixelFormat(&fmt);
|
||||||
if (FAILED(frame->WriteSource(bitmap.Get(), nullptr)) || FAILED(frame->Commit()) ||
|
if (FAILED(frame->WriteSource(bitmap.Get(), nullptr)) || FAILED(frame->Commit()) || FAILED(encoder->Commit())) {
|
||||||
FAILED(encoder->Commit()))
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -69,8 +59,7 @@ bool write_rgba8_png(const std::wstring& path, UINT width, UINT height, const BY
|
|||||||
D3D11Window::~D3D11Window()
|
D3D11Window::~D3D11Window()
|
||||||
{
|
{
|
||||||
release_render_target();
|
release_render_target();
|
||||||
if (hwnd_ != nullptr)
|
if (hwnd_ != nullptr) {
|
||||||
{
|
|
||||||
DestroyWindow(hwnd_);
|
DestroyWindow(hwnd_);
|
||||||
hwnd_ = nullptr;
|
hwnd_ = nullptr;
|
||||||
}
|
}
|
||||||
@@ -88,8 +77,7 @@ bool D3D11Window::create(const wchar_t* title)
|
|||||||
wc.hInstance = instance;
|
wc.hInstance = instance;
|
||||||
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||||
wc.lpszClassName = kWindowClass;
|
wc.lpszClassName = kWindowClass;
|
||||||
if (RegisterClassExW(&wc) == 0)
|
if (RegisterClassExW(&wc) == 0) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,13 +88,11 @@ bool D3D11Window::create(const wchar_t* title)
|
|||||||
const int height = GetSystemMetrics(SM_CYSCREEN);
|
const int height = GetSystemMetrics(SM_CYSCREEN);
|
||||||
|
|
||||||
hwnd_ = CreateWindowExW(0, kWindowClass, title, WS_POPUP, 0, 0, width, height, nullptr, nullptr, instance, this);
|
hwnd_ = CreateWindowExW(0, kWindowClass, title, WS_POPUP, 0, 0, width, height, nullptr, nullptr, instance, this);
|
||||||
if (hwnd_ == nullptr)
|
if (hwnd_ == nullptr) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!create_device())
|
if (!create_device()) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,30 +122,25 @@ bool D3D11Window::create_device()
|
|||||||
const D3D_FEATURE_LEVEL levels[] = {D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0};
|
const D3D_FEATURE_LEVEL levels[] = {D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0};
|
||||||
|
|
||||||
if (FAILED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, levels, _countof(levels),
|
if (FAILED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, levels, _countof(levels),
|
||||||
D3D11_SDK_VERSION, device_.GetAddressOf(), nullptr, context_.GetAddressOf())))
|
D3D11_SDK_VERSION, device_.GetAddressOf(), nullptr, context_.GetAddressOf()))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ComPtr<IDXGIDevice> dxgi_device;
|
ComPtr<IDXGIDevice> dxgi_device;
|
||||||
if (FAILED(device_.As(&dxgi_device)))
|
if (FAILED(device_.As(&dxgi_device))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ComPtr<IDXGIAdapter> adapter;
|
ComPtr<IDXGIAdapter> adapter;
|
||||||
if (FAILED(dxgi_device->GetAdapter(adapter.GetAddressOf())))
|
if (FAILED(dxgi_device->GetAdapter(adapter.GetAddressOf()))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ComPtr<IDXGIFactory2> factory;
|
ComPtr<IDXGIFactory2> factory;
|
||||||
if (FAILED(adapter->GetParent(IID_PPV_ARGS(factory.GetAddressOf()))))
|
if (FAILED(adapter->GetParent(IID_PPV_ARGS(factory.GetAddressOf())))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FAILED(factory->CreateSwapChainForHwnd(device_.Get(), hwnd_, &desc, nullptr, nullptr,
|
if (FAILED(factory->CreateSwapChainForHwnd(device_.Get(), hwnd_, &desc, nullptr, nullptr,
|
||||||
swap_chain_.GetAddressOf())))
|
swap_chain_.GetAddressOf()))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Don't let DXGI swallow Alt+Enter into an exclusive-fullscreen transition.
|
// Don't let DXGI swallow Alt+Enter into an exclusive-fullscreen transition.
|
||||||
@@ -171,8 +152,7 @@ bool D3D11Window::create_device()
|
|||||||
|
|
||||||
bool D3D11Window::note_device_loss(HRESULT hr)
|
bool D3D11Window::note_device_loss(HRESULT hr)
|
||||||
{
|
{
|
||||||
if (hr != DXGI_ERROR_DEVICE_REMOVED && hr != DXGI_ERROR_DEVICE_RESET)
|
if (hr != DXGI_ERROR_DEVICE_REMOVED && hr != DXGI_ERROR_DEVICE_RESET) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// GetDeviceRemovedReason gives the specific cause (HUNG / driver internal / removed); a plain
|
// GetDeviceRemovedReason gives the specific cause (HUNG / driver internal / removed); a plain
|
||||||
@@ -186,10 +166,8 @@ bool D3D11Window::note_device_loss(HRESULT hr)
|
|||||||
void D3D11Window::create_render_target()
|
void D3D11Window::create_render_target()
|
||||||
{
|
{
|
||||||
ComPtr<ID3D11Texture2D> back_buffer;
|
ComPtr<ID3D11Texture2D> back_buffer;
|
||||||
if (SUCCEEDED(swap_chain_->GetBuffer(0, IID_PPV_ARGS(back_buffer.GetAddressOf()))))
|
if (SUCCEEDED(swap_chain_->GetBuffer(0, IID_PPV_ARGS(back_buffer.GetAddressOf())))) {
|
||||||
{
|
const HRESULT hr = device_->CreateRenderTargetView(back_buffer.Get(), nullptr, rtv_.ReleaseAndGetAddressOf());
|
||||||
const HRESULT hr =
|
|
||||||
device_->CreateRenderTargetView(back_buffer.Get(), nullptr, rtv_.ReleaseAndGetAddressOf());
|
|
||||||
note_device_loss(hr); // a removed device surfaces here too; the render loop checks device_lost()
|
note_device_loss(hr); // a removed device surfaces here too; the render loop checks device_lost()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,14 +179,12 @@ void D3D11Window::release_render_target()
|
|||||||
|
|
||||||
void D3D11Window::handle_resize(UINT width, UINT height)
|
void D3D11Window::handle_resize(UINT width, UINT height)
|
||||||
{
|
{
|
||||||
if (swap_chain_ == nullptr || width == 0 || height == 0)
|
if (swap_chain_ == nullptr || width == 0 || height == 0) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
release_render_target();
|
release_render_target();
|
||||||
const HRESULT hr = swap_chain_->ResizeBuffers(0, width, height, DXGI_FORMAT_UNKNOWN, 0);
|
const HRESULT hr = swap_chain_->ResizeBuffers(0, width, height, DXGI_FORMAT_UNKNOWN, 0);
|
||||||
if (note_device_loss(hr))
|
if (note_device_loss(hr)) {
|
||||||
{
|
|
||||||
return; // device gone; the render loop will see device_lost() and stop
|
return; // device gone; the render loop will see device_lost() and stop
|
||||||
}
|
}
|
||||||
create_render_target();
|
create_render_target();
|
||||||
@@ -217,17 +193,14 @@ void D3D11Window::handle_resize(UINT width, UINT height)
|
|||||||
bool D3D11Window::pump_messages()
|
bool D3D11Window::pump_messages()
|
||||||
{
|
{
|
||||||
MSG msg;
|
MSG msg;
|
||||||
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
|
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
||||||
{
|
if (msg.message == WM_QUIT) {
|
||||||
if (msg.message == WM_QUIT)
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
TranslateMessage(&msg);
|
TranslateMessage(&msg);
|
||||||
DispatchMessageW(&msg);
|
DispatchMessageW(&msg);
|
||||||
}
|
}
|
||||||
if (resize_pending_)
|
if (resize_pending_) {
|
||||||
{
|
|
||||||
handle_resize(resize_width_, resize_height_);
|
handle_resize(resize_width_, resize_height_);
|
||||||
resize_pending_ = false;
|
resize_pending_ = false;
|
||||||
}
|
}
|
||||||
@@ -240,17 +213,14 @@ void D3D11Window::render_frame(const RenderCallback& render, UINT sync_interval)
|
|||||||
context_->OMSetRenderTargets(1, rtv_.GetAddressOf(), nullptr);
|
context_->OMSetRenderTargets(1, rtv_.GetAddressOf(), nullptr);
|
||||||
context_->ClearRenderTargetView(rtv_.Get(), clear);
|
context_->ClearRenderTargetView(rtv_.Get(), clear);
|
||||||
|
|
||||||
if (render)
|
if (render) {
|
||||||
{
|
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Screenshot (F10): capture after the overlay is drawn but before Present -- the
|
// Screenshot (F10): capture after the overlay is drawn but before Present -- the
|
||||||
// flip-model back buffer is undefined once presented.
|
// flip-model back buffer is undefined once presented.
|
||||||
if (!pending_screenshot_.empty())
|
if (!pending_screenshot_.empty()) {
|
||||||
{
|
if (save_backbuffer_png(pending_screenshot_)) {
|
||||||
if (save_backbuffer_png(pending_screenshot_))
|
|
||||||
{
|
|
||||||
saved_screenshot_ = pending_screenshot_;
|
saved_screenshot_ = pending_screenshot_;
|
||||||
}
|
}
|
||||||
pending_screenshot_.clear();
|
pending_screenshot_.clear();
|
||||||
@@ -277,8 +247,7 @@ std::wstring D3D11Window::take_screenshot_result()
|
|||||||
bool D3D11Window::save_backbuffer_png(const std::wstring& path)
|
bool D3D11Window::save_backbuffer_png(const std::wstring& path)
|
||||||
{
|
{
|
||||||
ComPtr<ID3D11Texture2D> back;
|
ComPtr<ID3D11Texture2D> back;
|
||||||
if (FAILED(swap_chain_->GetBuffer(0, IID_PPV_ARGS(back.GetAddressOf()))))
|
if (FAILED(swap_chain_->GetBuffer(0, IID_PPV_ARGS(back.GetAddressOf())))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
D3D11_TEXTURE2D_DESC desc{};
|
D3D11_TEXTURE2D_DESC desc{};
|
||||||
@@ -291,44 +260,37 @@ bool D3D11Window::save_backbuffer_png(const std::wstring& path)
|
|||||||
staging.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
staging.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||||
staging.MiscFlags = 0;
|
staging.MiscFlags = 0;
|
||||||
ComPtr<ID3D11Texture2D> cpu;
|
ComPtr<ID3D11Texture2D> cpu;
|
||||||
if (FAILED(device_->CreateTexture2D(&staging, nullptr, cpu.GetAddressOf())))
|
if (FAILED(device_->CreateTexture2D(&staging, nullptr, cpu.GetAddressOf()))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
context_->CopyResource(cpu.Get(), back.Get());
|
context_->CopyResource(cpu.Get(), back.Get());
|
||||||
|
|
||||||
D3D11_MAPPED_SUBRESOURCE map{};
|
D3D11_MAPPED_SUBRESOURCE map{};
|
||||||
if (FAILED(context_->Map(cpu.Get(), 0, D3D11_MAP_READ, 0, &map)))
|
if (FAILED(context_->Map(cpu.Get(), 0, D3D11_MAP_READ, 0, &map))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// The swap chain is DXGI_FORMAT_R8G8B8A8_UNORM (see create_device), i.e. RGBA bytes.
|
// The swap chain is DXGI_FORMAT_R8G8B8A8_UNORM (see create_device), i.e. RGBA bytes.
|
||||||
const bool ok =
|
const bool ok = write_rgba8_png(path, desc.Width, desc.Height, static_cast<const BYTE*>(map.pData), map.RowPitch);
|
||||||
write_rgba8_png(path, desc.Width, desc.Height, static_cast<const BYTE*>(map.pData), map.RowPitch);
|
|
||||||
context_->Unmap(cpu.Get(), 0);
|
context_->Unmap(cpu.Get(), 0);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
LRESULT CALLBACK D3D11Window::wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
|
LRESULT CALLBACK D3D11Window::wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
|
||||||
{
|
{
|
||||||
if (msg == WM_NCCREATE)
|
if (msg == WM_NCCREATE) {
|
||||||
{
|
|
||||||
auto* create = reinterpret_cast<CREATESTRUCTW*>(lparam);
|
auto* create = reinterpret_cast<CREATESTRUCTW*>(lparam);
|
||||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(create->lpCreateParams));
|
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(create->lpCreateParams));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui_ImplWin32_WndProcHandler(hwnd, msg, wparam, lparam))
|
if (ImGui_ImplWin32_WndProcHandler(hwnd, msg, wparam, lparam)) {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* self = reinterpret_cast<D3D11Window*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
auto* self = reinterpret_cast<D3D11Window*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||||
|
|
||||||
switch (msg)
|
switch (msg) {
|
||||||
{
|
|
||||||
case WM_SIZE:
|
case WM_SIZE:
|
||||||
if (self != nullptr && wparam != SIZE_MINIMIZED)
|
if (self != nullptr && wparam != SIZE_MINIMIZED) {
|
||||||
{
|
|
||||||
self->resize_pending_ = true;
|
self->resize_pending_ = true;
|
||||||
self->resize_width_ = LOWORD(lparam);
|
self->resize_width_ = LOWORD(lparam);
|
||||||
self->resize_height_ = HIWORD(lparam);
|
self->resize_height_ = HIWORD(lparam);
|
||||||
@@ -338,13 +300,10 @@ LRESULT CALLBACK D3D11Window::wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARA
|
|||||||
// Per-monitor-v2: the DPI of the display we're on changed. Resize to the rect Windows suggests
|
// Per-monitor-v2: the DPI of the display we're on changed. Resize to the rect Windows suggests
|
||||||
// in lparam (its recommended handling; the resulting WM_SIZE repaints the swap chain via the
|
// in lparam (its recommended handling; the resulting WM_SIZE repaints the swap chain via the
|
||||||
// deferred-resize path above), then latch the new DPI for the overlay to rescale its font/style.
|
// deferred-resize path above), then latch the new DPI for the overlay to rescale its font/style.
|
||||||
if (self != nullptr)
|
if (self != nullptr) {
|
||||||
{
|
if (const auto* suggested = reinterpret_cast<const RECT*>(lparam); suggested != nullptr) {
|
||||||
if (const auto* suggested = reinterpret_cast<const RECT*>(lparam); suggested != nullptr)
|
SetWindowPos(hwnd, nullptr, suggested->left, suggested->top, suggested->right - suggested->left,
|
||||||
{
|
suggested->bottom - suggested->top, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||||
SetWindowPos(hwnd, nullptr, suggested->left, suggested->top,
|
|
||||||
suggested->right - suggested->left, suggested->bottom - suggested->top,
|
|
||||||
SWP_NOZORDER | SWP_NOACTIVATE);
|
|
||||||
}
|
}
|
||||||
self->dpi_pending_ = true;
|
self->dpi_pending_ = true;
|
||||||
self->pending_dpi_ = HIWORD(wparam); // X and Y DPI are equal; HIWORD is the Y value
|
self->pending_dpi_ = HIWORD(wparam); // X and Y DPI are equal; HIWORD is the Y value
|
||||||
@@ -354,8 +313,7 @@ LRESULT CALLBACK D3D11Window::wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARA
|
|||||||
// F10 is our screenshot key; ImGui already saw this message (handler runs above),
|
// F10 is our screenshot key; ImGui already saw this message (handler runs above),
|
||||||
// so swallow it here to stop DefWindowProc from flicking into Win32 menu mode.
|
// so swallow it here to stop DefWindowProc from flicking into Win32 menu mode.
|
||||||
// Alt+F4 (VK_F4) falls through to DefWindowProc so it still closes the window.
|
// Alt+F4 (VK_F4) falls through to DefWindowProc so it still closes the window.
|
||||||
if (wparam == VK_F10)
|
if (wparam == VK_F10) {
|
||||||
{
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -10,11 +10,9 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
class D3D11Window
|
class D3D11Window {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
using RenderCallback = std::function<void()>;
|
using RenderCallback = std::function<void()>;
|
||||||
|
|
||||||
@@ -50,15 +48,9 @@ public:
|
|||||||
// True once Present/ResizeBuffers reported DXGI_ERROR_DEVICE_REMOVED/RESET (a host-side TDR,
|
// True once Present/ResizeBuffers reported DXGI_ERROR_DEVICE_REMOVED/RESET (a host-side TDR,
|
||||||
// driver reset, or GPU hang). The render loop is expected to stop and surface the error rather
|
// driver reset, or GPU hang). The render loop is expected to stop and surface the error rather
|
||||||
// than spin forever on a dead device; full device re-creation is intentionally not attempted.
|
// than spin forever on a dead device; full device re-creation is intentionally not attempted.
|
||||||
[[nodiscard]] bool device_lost() const
|
[[nodiscard]] bool device_lost() const { return device_lost_; }
|
||||||
{
|
|
||||||
return device_lost_;
|
|
||||||
}
|
|
||||||
// The GetDeviceRemovedReason() HRESULT (or the originating error) when device_lost() is true.
|
// The GetDeviceRemovedReason() HRESULT (or the originating error) when device_lost() is true.
|
||||||
[[nodiscard]] HRESULT device_lost_reason() const
|
[[nodiscard]] HRESULT device_lost_reason() const { return device_lost_reason_; }
|
||||||
{
|
|
||||||
return device_lost_reason_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a WM_DPICHANGED arrived since the last call (the window moved to a different-DPI monitor, or
|
// If a WM_DPICHANGED arrived since the last call (the window moved to a different-DPI monitor, or
|
||||||
// the display scale changed at runtime), returns true and writes that monitor's DPI to `dpi`,
|
// the display scale changed at runtime), returns true and writes that monitor's DPI to `dpi`,
|
||||||
@@ -66,8 +58,7 @@ public:
|
|||||||
// fonts/style. One-shot, mirroring the deferred-resize handling in pump_messages().
|
// fonts/style. One-shot, mirroring the deferred-resize handling in pump_messages().
|
||||||
[[nodiscard]] bool take_dpi_change(unsigned& dpi)
|
[[nodiscard]] bool take_dpi_change(unsigned& dpi)
|
||||||
{
|
{
|
||||||
if (!dpi_pending_)
|
if (!dpi_pending_) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
dpi = pending_dpi_;
|
dpi = pending_dpi_;
|
||||||
@@ -75,18 +66,9 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] HWND hwnd() const
|
[[nodiscard]] HWND hwnd() const { return hwnd_; }
|
||||||
{
|
[[nodiscard]] ID3D11Device* device() const { return device_.Get(); }
|
||||||
return hwnd_;
|
[[nodiscard]] ID3D11DeviceContext* context() const { return context_.Get(); }
|
||||||
}
|
|
||||||
[[nodiscard]] ID3D11Device* device() const
|
|
||||||
{
|
|
||||||
return device_.Get();
|
|
||||||
}
|
|
||||||
[[nodiscard]] ID3D11DeviceContext* context() const
|
|
||||||
{
|
|
||||||
return context_.Get();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
|
static LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
|
||||||
|
|||||||
@@ -11,13 +11,11 @@
|
|||||||
#include "ui/app_chrome.hpp"
|
#include "ui/app_chrome.hpp"
|
||||||
#include "util/utf8.hpp"
|
#include "util/utf8.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
ImGuiLayer::~ImGuiLayer()
|
ImGuiLayer::~ImGuiLayer()
|
||||||
{
|
{
|
||||||
if (initialized_)
|
if (initialized_) {
|
||||||
{
|
|
||||||
ImGui_ImplDX11_Shutdown();
|
ImGui_ImplDX11_Shutdown();
|
||||||
ImGui_ImplWin32_Shutdown();
|
ImGui_ImplWin32_Shutdown();
|
||||||
ImGui::DestroyContext();
|
ImGui::DestroyContext();
|
||||||
@@ -42,12 +40,10 @@ bool ImGuiLayer::init(HWND hwnd, ID3D11Device* device, ID3D11DeviceContext* cont
|
|||||||
io.IniFilename = ini_path_.c_str();
|
io.IniFilename = ini_path_.c_str();
|
||||||
set_layout_persisted(had_layout);
|
set_layout_persisted(had_layout);
|
||||||
|
|
||||||
if (!ImGui_ImplWin32_Init(hwnd))
|
if (!ImGui_ImplWin32_Init(hwnd)) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ImGui_ImplDX11_Init(device, context))
|
if (!ImGui_ImplDX11_Init(device, context)) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
initialized_ = true;
|
initialized_ = true;
|
||||||
@@ -78,8 +74,7 @@ void ImGuiLayer::apply_dpi(unsigned dpi)
|
|||||||
|
|
||||||
// Drop the DX11 backend's cached font texture so it rebuilds from the new atlas next frame. Before
|
// Drop the DX11 backend's cached font texture so it rebuilds from the new atlas next frame. Before
|
||||||
// the first frame nothing is built yet, so this is a harmless no-op during init().
|
// the first frame nothing is built yet, so this is a harmless no-op during init().
|
||||||
if (initialized_)
|
if (initialized_) {
|
||||||
{
|
|
||||||
ImGui_ImplDX11_InvalidateDeviceObjects();
|
ImGui_ImplDX11_InvalidateDeviceObjects();
|
||||||
}
|
}
|
||||||
dpi_scale_ = scale;
|
dpi_scale_ = scale;
|
||||||
@@ -87,8 +82,7 @@ void ImGuiLayer::apply_dpi(unsigned dpi)
|
|||||||
|
|
||||||
void ImGuiLayer::set_dpi(unsigned dpi)
|
void ImGuiLayer::set_dpi(unsigned dpi)
|
||||||
{
|
{
|
||||||
if (!initialized_ || dpi_scale_from(dpi) == dpi_scale_)
|
if (!initialized_ || dpi_scale_from(dpi) == dpi_scale_) {
|
||||||
{
|
|
||||||
return; // not up yet, or the scale didn't actually change -- skip a needless atlas rebuild
|
return; // not up yet, or the scale didn't actually change -- skip a needless atlas rebuild
|
||||||
}
|
}
|
||||||
apply_dpi(dpi);
|
apply_dpi(dpi);
|
||||||
|
|||||||
@@ -6,11 +6,9 @@
|
|||||||
#include <d3d11.h>
|
#include <d3d11.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
class ImGuiLayer
|
class ImGuiLayer {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
ImGuiLayer() = default;
|
ImGuiLayer() = default;
|
||||||
~ImGuiLayer();
|
~ImGuiLayer();
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
#include "coop/protocol.hpp"
|
#include "coop/protocol.hpp"
|
||||||
#include "coop/shared_memory.hpp"
|
#include "coop/shared_memory.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
bool hook_dll_alive(unsigned long pid, int timeout_ms)
|
bool hook_dll_alive(unsigned long pid, int timeout_ms)
|
||||||
{
|
{
|
||||||
@@ -18,17 +17,14 @@ bool hook_dll_alive(unsigned long pid, int timeout_ms)
|
|||||||
// Poll rather than sample once: the worker only beats ~4x/s, so a single short read can straddle
|
// Poll rather than sample once: the worker only beats ~4x/s, so a single short read can straddle
|
||||||
// a gap and miss it; return the instant a beat lands, and give up after the timeout.
|
// a gap and miss it; return the instant a beat lands, and give up after the timeout.
|
||||||
SharedMemory shm;
|
SharedMemory shm;
|
||||||
if (!shm.open(shared_memory_name(pid), sizeof(SharedBlock)))
|
if (!shm.open(shared_memory_name(pid), sizeof(SharedBlock))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto* block = shm.as<SharedBlock>();
|
auto* block = shm.as<SharedBlock>();
|
||||||
const std::uint32_t h0 = block->status.heartbeat.load(std::memory_order_acquire);
|
const std::uint32_t h0 = block->status.heartbeat.load(std::memory_order_acquire);
|
||||||
for (int waited = 0; waited < timeout_ms; waited += 25)
|
for (int waited = 0; waited < timeout_ms; waited += 25) {
|
||||||
{
|
|
||||||
Sleep(25);
|
Sleep(25);
|
||||||
if (block->status.heartbeat.load(std::memory_order_acquire) != h0)
|
if (block->status.heartbeat.load(std::memory_order_acquire) != h0) {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
// crash, since a connected DLL keeps the per-pid IPC section alive.
|
// crash, since a connected DLL keeps the per-pid IPC section alive.
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// True if `pid` already hosts a live coop_hook DLL: the per-pid IPC section exists and its heartbeat
|
// True if `pid` already hosts a live coop_hook DLL: the per-pid IPC section exists and its heartbeat
|
||||||
// advances within `timeout_ms` (the DLL's worker is still beating). Returns as soon as a beat lands,
|
// advances within `timeout_ms` (the DLL's worker is still beating). Returns as soon as a beat lands,
|
||||||
|
|||||||
@@ -2,13 +2,11 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
const char* to_string(InjectStatus status)
|
const char* to_string(InjectStatus status)
|
||||||
{
|
{
|
||||||
switch (status)
|
switch (status) {
|
||||||
{
|
|
||||||
case InjectStatus::Ok:
|
case InjectStatus::Ok:
|
||||||
return "OK";
|
return "OK";
|
||||||
case InjectStatus::OpenProcessFailed:
|
case InjectStatus::OpenProcessFailed:
|
||||||
@@ -33,8 +31,7 @@ const char* to_string(InjectStatus status)
|
|||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
InjectResult fail(InjectStatus status)
|
InjectResult fail(InjectStatus status)
|
||||||
{
|
{
|
||||||
@@ -46,16 +43,14 @@ bool is_wow64_process(HANDLE process)
|
|||||||
{
|
{
|
||||||
USHORT process_machine = IMAGE_FILE_MACHINE_UNKNOWN;
|
USHORT process_machine = IMAGE_FILE_MACHINE_UNKNOWN;
|
||||||
USHORT native_machine = IMAGE_FILE_MACHINE_UNKNOWN;
|
USHORT native_machine = IMAGE_FILE_MACHINE_UNKNOWN;
|
||||||
if (IsWow64Process2(process, &process_machine, &native_machine))
|
if (IsWow64Process2(process, &process_machine, &native_machine)) {
|
||||||
{
|
|
||||||
return process_machine != IMAGE_FILE_MACHINE_UNKNOWN;
|
return process_machine != IMAGE_FILE_MACHINE_UNKNOWN;
|
||||||
}
|
}
|
||||||
// IsWow64Process2 failed -- fall back to the legacy query rather than guessing "native", since
|
// IsWow64Process2 failed -- fall back to the legacy query rather than guessing "native", since
|
||||||
// guessing wrong sends the x64 DLL into a 32-bit target (which can't load it). Only if BOTH
|
// guessing wrong sends the x64 DLL into a 32-bit target (which can't load it). Only if BOTH
|
||||||
// queries fail do we fall back to permissive.
|
// queries fail do we fall back to permissive.
|
||||||
BOOL wow64 = FALSE;
|
BOOL wow64 = FALSE;
|
||||||
if (IsWow64Process(process, &wow64))
|
if (IsWow64Process(process, &wow64)) {
|
||||||
{
|
|
||||||
return wow64 != FALSE;
|
return wow64 != FALSE;
|
||||||
}
|
}
|
||||||
return false; // both queries failed; best-effort assume native
|
return false; // both queries failed; best-effort assume native
|
||||||
@@ -75,9 +70,8 @@ InjectResult inject_via_helper(unsigned long pid, const std::wstring& dll_path)
|
|||||||
{
|
{
|
||||||
const std::wstring helper = sibling(dll_path, L"coop_inject_x86.exe");
|
const std::wstring helper = sibling(dll_path, L"coop_inject_x86.exe");
|
||||||
const std::wstring x86_dll = sibling(dll_path, L"coop_hook_x86.dll");
|
const std::wstring x86_dll = sibling(dll_path, L"coop_hook_x86.dll");
|
||||||
if (GetFileAttributesW(helper.c_str()) == INVALID_FILE_ATTRIBUTES ||
|
if (GetFileAttributesW(helper.c_str()) == INVALID_FILE_ATTRIBUTES
|
||||||
GetFileAttributesW(x86_dll.c_str()) == INVALID_FILE_ATTRIBUTES)
|
|| GetFileAttributesW(x86_dll.c_str()) == INVALID_FILE_ATTRIBUTES) {
|
||||||
{
|
|
||||||
return InjectResult{InjectStatus::HelperNotFound, 0};
|
return InjectResult{InjectStatus::HelperNotFound, 0};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,9 +81,8 @@ InjectResult inject_via_helper(unsigned long pid, const std::wstring& dll_path)
|
|||||||
STARTUPINFOW si{};
|
STARTUPINFOW si{};
|
||||||
si.cb = sizeof(si);
|
si.cb = sizeof(si);
|
||||||
PROCESS_INFORMATION pi{};
|
PROCESS_INFORMATION pi{};
|
||||||
if (!CreateProcessW(helper.c_str(), cmd.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr,
|
if (!CreateProcessW(helper.c_str(), cmd.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &si,
|
||||||
&si, &pi))
|
&pi)) {
|
||||||
{
|
|
||||||
return fail(InjectStatus::HelperFailed);
|
return fail(InjectStatus::HelperFailed);
|
||||||
}
|
}
|
||||||
WaitForSingleObject(pi.hProcess, INFINITE);
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||||
@@ -98,8 +91,7 @@ InjectResult inject_via_helper(unsigned long pid, const std::wstring& dll_path)
|
|||||||
const DWORD err = got ? exit_code : GetLastError(); // on a failed query, surface the OS error
|
const DWORD err = got ? exit_code : GetLastError(); // on a failed query, surface the OS error
|
||||||
CloseHandle(pi.hThread);
|
CloseHandle(pi.hThread);
|
||||||
CloseHandle(pi.hProcess);
|
CloseHandle(pi.hProcess);
|
||||||
if (!got || exit_code != 0)
|
if (!got || exit_code != 0) {
|
||||||
{
|
|
||||||
return InjectResult{InjectStatus::HelperFailed, err};
|
return InjectResult{InjectStatus::HelperFailed, err};
|
||||||
}
|
}
|
||||||
return InjectResult{InjectStatus::Ok, 0};
|
return InjectResult{InjectStatus::Ok, 0};
|
||||||
@@ -109,63 +101,51 @@ InjectResult inject_via_helper(unsigned long pid, const std::wstring& dll_path)
|
|||||||
|
|
||||||
InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path)
|
InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path)
|
||||||
{
|
{
|
||||||
if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES)
|
if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES) {
|
||||||
{
|
|
||||||
return fail(InjectStatus::DllNotFound);
|
return fail(InjectStatus::DllNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DWORD access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION |
|
const DWORD access =
|
||||||
PROCESS_VM_WRITE | PROCESS_VM_READ;
|
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ;
|
||||||
HANDLE process = OpenProcess(access, FALSE, pid);
|
HANDLE process = OpenProcess(access, FALSE, pid);
|
||||||
if (process == nullptr)
|
if (process == nullptr) {
|
||||||
{
|
|
||||||
return fail(InjectStatus::OpenProcessFailed);
|
return fail(InjectStatus::OpenProcessFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HandleGuard
|
struct HandleGuard {
|
||||||
{
|
|
||||||
HANDLE h;
|
HANDLE h;
|
||||||
~HandleGuard()
|
~HandleGuard()
|
||||||
{
|
{
|
||||||
if (h != nullptr)
|
if (h != nullptr) {
|
||||||
{
|
|
||||||
CloseHandle(h);
|
CloseHandle(h);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} process_guard{process};
|
} process_guard{process};
|
||||||
|
|
||||||
if (is_wow64_process(process))
|
if (is_wow64_process(process)) {
|
||||||
{
|
|
||||||
// The x64 host can't inject a 32-bit target directly; delegate to the helper.
|
// The x64 host can't inject a 32-bit target directly; delegate to the helper.
|
||||||
return inject_via_helper(pid, dll_path);
|
return inject_via_helper(pid, dll_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SIZE_T bytes = (dll_path.size() + 1) * sizeof(wchar_t);
|
const SIZE_T bytes = (dll_path.size() + 1) * sizeof(wchar_t);
|
||||||
void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||||
if (remote == nullptr)
|
if (remote == nullptr) {
|
||||||
{
|
|
||||||
return fail(InjectStatus::AllocFailed);
|
return fail(InjectStatus::AllocFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
InjectResult result{InjectStatus::Ok, 0};
|
InjectResult result{InjectStatus::Ok, 0};
|
||||||
|
|
||||||
if (!WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr))
|
if (!WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr)) {
|
||||||
{
|
|
||||||
result = fail(InjectStatus::WriteFailed);
|
result = fail(InjectStatus::WriteFailed);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
// kernel32 is mapped at the same address in every process, so LoadLibraryW's
|
// kernel32 is mapped at the same address in every process, so LoadLibraryW's
|
||||||
// address in this process is valid as the remote thread's start routine.
|
// address in this process is valid as the remote thread's start routine.
|
||||||
auto load_library =
|
auto load_library =
|
||||||
reinterpret_cast<LPTHREAD_START_ROUTINE>(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"));
|
reinterpret_cast<LPTHREAD_START_ROUTINE>(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"));
|
||||||
HANDLE thread = CreateRemoteThread(process, nullptr, 0, load_library, remote, 0, nullptr);
|
HANDLE thread = CreateRemoteThread(process, nullptr, 0, load_library, remote, 0, nullptr);
|
||||||
if (thread == nullptr)
|
if (thread == nullptr) {
|
||||||
{
|
|
||||||
result = fail(InjectStatus::RemoteThreadFailed);
|
result = fail(InjectStatus::RemoteThreadFailed);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
WaitForSingleObject(thread, INFINITE);
|
WaitForSingleObject(thread, INFINITE);
|
||||||
DWORD exit_code = 0;
|
DWORD exit_code = 0;
|
||||||
GetExitCodeThread(thread, &exit_code);
|
GetExitCodeThread(thread, &exit_code);
|
||||||
@@ -173,8 +153,7 @@ InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path)
|
|||||||
// LoadLibraryW returns the module handle; 0 means it failed to load.
|
// LoadLibraryW returns the module handle; 0 means it failed to load.
|
||||||
// (The handle is truncated to 32 bits here, but zero vs non-zero is
|
// (The handle is truncated to 32 bits here, but zero vs non-zero is
|
||||||
// all we need to distinguish success from failure.)
|
// all we need to distinguish success from failure.)
|
||||||
if (exit_code == 0)
|
if (exit_code == 0) {
|
||||||
{
|
|
||||||
result = InjectResult{InjectStatus::RemoteLoadFailed, 0};
|
result = InjectResult{InjectStatus::RemoteLoadFailed, 0};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,9 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
enum class InjectStatus
|
enum class InjectStatus {
|
||||||
{
|
|
||||||
Ok,
|
Ok,
|
||||||
OpenProcessFailed, // insufficient rights (try running the host as admin)
|
OpenProcessFailed, // insufficient rights (try running the host as admin)
|
||||||
BitnessMismatch, // 32-bit target; the x86 hook/helper aren't available
|
BitnessMismatch, // 32-bit target; the x86 hook/helper aren't available
|
||||||
@@ -21,8 +19,7 @@ enum class InjectStatus
|
|||||||
HelperFailed, // the x86 injector helper ran but reported failure
|
HelperFailed, // the x86 injector helper ran but reported failure
|
||||||
};
|
};
|
||||||
|
|
||||||
struct InjectResult
|
struct InjectResult {
|
||||||
{
|
|
||||||
InjectStatus status = InjectStatus::OpenProcessFailed;
|
InjectStatus status = InjectStatus::OpenProcessFailed;
|
||||||
unsigned long os_error = 0; // GetLastError at the point of failure, if any
|
unsigned long os_error = 0; // GetLastError at the point of failure, if any
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,80 +6,120 @@
|
|||||||
#include "injection_panel.hpp"
|
#include "injection_panel.hpp"
|
||||||
#include "inject/mkb_map.hpp"
|
#include "inject/mkb_map.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
// Map an ImGui key to a Win32 virtual-key. Returns 0 for keys we don't forward.
|
// Map an ImGui key to a Win32 virtual-key. Returns 0 for keys we don't forward.
|
||||||
int imgui_key_to_vk(ImGuiKey k)
|
int imgui_key_to_vk(ImGuiKey k)
|
||||||
{
|
{
|
||||||
if (k >= ImGuiKey_A && k <= ImGuiKey_Z)
|
if (k >= ImGuiKey_A && k <= ImGuiKey_Z) {
|
||||||
{
|
|
||||||
return 'A' + (k - ImGuiKey_A);
|
return 'A' + (k - ImGuiKey_A);
|
||||||
}
|
}
|
||||||
if (k >= ImGuiKey_0 && k <= ImGuiKey_9)
|
if (k >= ImGuiKey_0 && k <= ImGuiKey_9) {
|
||||||
{
|
|
||||||
return '0' + (k - ImGuiKey_0);
|
return '0' + (k - ImGuiKey_0);
|
||||||
}
|
}
|
||||||
if (k >= ImGuiKey_Keypad0 && k <= ImGuiKey_Keypad9)
|
if (k >= ImGuiKey_Keypad0 && k <= ImGuiKey_Keypad9) {
|
||||||
{
|
|
||||||
return VK_NUMPAD0 + (k - ImGuiKey_Keypad0);
|
return VK_NUMPAD0 + (k - ImGuiKey_Keypad0);
|
||||||
}
|
}
|
||||||
if (k >= ImGuiKey_F1 && k <= ImGuiKey_F12)
|
if (k >= ImGuiKey_F1 && k <= ImGuiKey_F12) {
|
||||||
{
|
|
||||||
return VK_F1 + (k - ImGuiKey_F1);
|
return VK_F1 + (k - ImGuiKey_F1);
|
||||||
}
|
}
|
||||||
switch (k)
|
switch (k) {
|
||||||
{
|
case ImGuiKey_Tab:
|
||||||
case ImGuiKey_Tab: return VK_TAB;
|
return VK_TAB;
|
||||||
case ImGuiKey_LeftArrow: return VK_LEFT;
|
case ImGuiKey_LeftArrow:
|
||||||
case ImGuiKey_RightArrow: return VK_RIGHT;
|
return VK_LEFT;
|
||||||
case ImGuiKey_UpArrow: return VK_UP;
|
case ImGuiKey_RightArrow:
|
||||||
case ImGuiKey_DownArrow: return VK_DOWN;
|
return VK_RIGHT;
|
||||||
case ImGuiKey_PageUp: return VK_PRIOR;
|
case ImGuiKey_UpArrow:
|
||||||
case ImGuiKey_PageDown: return VK_NEXT;
|
return VK_UP;
|
||||||
case ImGuiKey_Home: return VK_HOME;
|
case ImGuiKey_DownArrow:
|
||||||
case ImGuiKey_End: return VK_END;
|
return VK_DOWN;
|
||||||
case ImGuiKey_Insert: return VK_INSERT;
|
case ImGuiKey_PageUp:
|
||||||
case ImGuiKey_Delete: return VK_DELETE;
|
return VK_PRIOR;
|
||||||
case ImGuiKey_Backspace: return VK_BACK;
|
case ImGuiKey_PageDown:
|
||||||
case ImGuiKey_Space: return VK_SPACE;
|
return VK_NEXT;
|
||||||
case ImGuiKey_Enter: return VK_RETURN;
|
case ImGuiKey_Home:
|
||||||
case ImGuiKey_Escape: return VK_ESCAPE;
|
return VK_HOME;
|
||||||
case ImGuiKey_LeftCtrl: return VK_LCONTROL;
|
case ImGuiKey_End:
|
||||||
case ImGuiKey_LeftShift: return VK_LSHIFT;
|
return VK_END;
|
||||||
case ImGuiKey_LeftAlt: return VK_LMENU;
|
case ImGuiKey_Insert:
|
||||||
case ImGuiKey_LeftSuper: return VK_LWIN;
|
return VK_INSERT;
|
||||||
case ImGuiKey_RightCtrl: return VK_RCONTROL;
|
case ImGuiKey_Delete:
|
||||||
case ImGuiKey_RightShift: return VK_RSHIFT;
|
return VK_DELETE;
|
||||||
case ImGuiKey_RightAlt: return VK_RMENU;
|
case ImGuiKey_Backspace:
|
||||||
case ImGuiKey_RightSuper: return VK_RWIN;
|
return VK_BACK;
|
||||||
case ImGuiKey_Menu: return VK_APPS;
|
case ImGuiKey_Space:
|
||||||
case ImGuiKey_Apostrophe: return VK_OEM_7;
|
return VK_SPACE;
|
||||||
case ImGuiKey_Comma: return VK_OEM_COMMA;
|
case ImGuiKey_Enter:
|
||||||
case ImGuiKey_Minus: return VK_OEM_MINUS;
|
return VK_RETURN;
|
||||||
case ImGuiKey_Period: return VK_OEM_PERIOD;
|
case ImGuiKey_Escape:
|
||||||
case ImGuiKey_Slash: return VK_OEM_2;
|
return VK_ESCAPE;
|
||||||
case ImGuiKey_Semicolon: return VK_OEM_1;
|
case ImGuiKey_LeftCtrl:
|
||||||
case ImGuiKey_Equal: return VK_OEM_PLUS;
|
return VK_LCONTROL;
|
||||||
case ImGuiKey_LeftBracket: return VK_OEM_4;
|
case ImGuiKey_LeftShift:
|
||||||
case ImGuiKey_Backslash: return VK_OEM_5;
|
return VK_LSHIFT;
|
||||||
case ImGuiKey_RightBracket: return VK_OEM_6;
|
case ImGuiKey_LeftAlt:
|
||||||
case ImGuiKey_GraveAccent: return VK_OEM_3;
|
return VK_LMENU;
|
||||||
case ImGuiKey_CapsLock: return VK_CAPITAL;
|
case ImGuiKey_LeftSuper:
|
||||||
case ImGuiKey_ScrollLock: return VK_SCROLL;
|
return VK_LWIN;
|
||||||
case ImGuiKey_NumLock: return VK_NUMLOCK;
|
case ImGuiKey_RightCtrl:
|
||||||
case ImGuiKey_PrintScreen: return VK_SNAPSHOT;
|
return VK_RCONTROL;
|
||||||
case ImGuiKey_Pause: return VK_PAUSE;
|
case ImGuiKey_RightShift:
|
||||||
case ImGuiKey_KeypadDecimal: return VK_DECIMAL;
|
return VK_RSHIFT;
|
||||||
case ImGuiKey_KeypadDivide: return VK_DIVIDE;
|
case ImGuiKey_RightAlt:
|
||||||
case ImGuiKey_KeypadMultiply: return VK_MULTIPLY;
|
return VK_RMENU;
|
||||||
case ImGuiKey_KeypadSubtract: return VK_SUBTRACT;
|
case ImGuiKey_RightSuper:
|
||||||
case ImGuiKey_KeypadAdd: return VK_ADD;
|
return VK_RWIN;
|
||||||
case ImGuiKey_KeypadEnter: return VK_RETURN;
|
case ImGuiKey_Menu:
|
||||||
default: return 0;
|
return VK_APPS;
|
||||||
|
case ImGuiKey_Apostrophe:
|
||||||
|
return VK_OEM_7;
|
||||||
|
case ImGuiKey_Comma:
|
||||||
|
return VK_OEM_COMMA;
|
||||||
|
case ImGuiKey_Minus:
|
||||||
|
return VK_OEM_MINUS;
|
||||||
|
case ImGuiKey_Period:
|
||||||
|
return VK_OEM_PERIOD;
|
||||||
|
case ImGuiKey_Slash:
|
||||||
|
return VK_OEM_2;
|
||||||
|
case ImGuiKey_Semicolon:
|
||||||
|
return VK_OEM_1;
|
||||||
|
case ImGuiKey_Equal:
|
||||||
|
return VK_OEM_PLUS;
|
||||||
|
case ImGuiKey_LeftBracket:
|
||||||
|
return VK_OEM_4;
|
||||||
|
case ImGuiKey_Backslash:
|
||||||
|
return VK_OEM_5;
|
||||||
|
case ImGuiKey_RightBracket:
|
||||||
|
return VK_OEM_6;
|
||||||
|
case ImGuiKey_GraveAccent:
|
||||||
|
return VK_OEM_3;
|
||||||
|
case ImGuiKey_CapsLock:
|
||||||
|
return VK_CAPITAL;
|
||||||
|
case ImGuiKey_ScrollLock:
|
||||||
|
return VK_SCROLL;
|
||||||
|
case ImGuiKey_NumLock:
|
||||||
|
return VK_NUMLOCK;
|
||||||
|
case ImGuiKey_PrintScreen:
|
||||||
|
return VK_SNAPSHOT;
|
||||||
|
case ImGuiKey_Pause:
|
||||||
|
return VK_PAUSE;
|
||||||
|
case ImGuiKey_KeypadDecimal:
|
||||||
|
return VK_DECIMAL;
|
||||||
|
case ImGuiKey_KeypadDivide:
|
||||||
|
return VK_DIVIDE;
|
||||||
|
case ImGuiKey_KeypadMultiply:
|
||||||
|
return VK_MULTIPLY;
|
||||||
|
case ImGuiKey_KeypadSubtract:
|
||||||
|
return VK_SUBTRACT;
|
||||||
|
case ImGuiKey_KeypadAdd:
|
||||||
|
return VK_ADD;
|
||||||
|
case ImGuiKey_KeypadEnter:
|
||||||
|
return VK_RETURN;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,10 +135,8 @@ bool g_key_down[256] = {}; // indexed by VK
|
|||||||
|
|
||||||
void release_held_keys(InjectionPanel& injection)
|
void release_held_keys(InjectionPanel& injection)
|
||||||
{
|
{
|
||||||
for (int vk = 0; vk < 256; ++vk)
|
for (int vk = 0; vk < 256; ++vk) {
|
||||||
{
|
if (g_key_down[vk]) {
|
||||||
if (g_key_down[vk])
|
|
||||||
{
|
|
||||||
injection.push_mkb(MkbEvent{Mkb_KeyUp, static_cast<std::uint32_t>(vk), 0, 0});
|
injection.push_mkb(MkbEvent{Mkb_KeyUp, static_cast<std::uint32_t>(vk), 0, 0});
|
||||||
g_key_down[vk] = false;
|
g_key_down[vk] = false;
|
||||||
}
|
}
|
||||||
@@ -107,10 +145,8 @@ void release_held_keys(InjectionPanel& injection)
|
|||||||
|
|
||||||
void release_held_mouse(InjectionPanel& injection)
|
void release_held_mouse(InjectionPanel& injection)
|
||||||
{
|
{
|
||||||
for (int b = 0; b < 3; ++b)
|
for (int b = 0; b < 3; ++b) {
|
||||||
{
|
if (g_mouse_down[b]) {
|
||||||
if (g_mouse_down[b])
|
|
||||||
{
|
|
||||||
injection.push_mkb(MkbEvent{Mkb_MouseUp, static_cast<std::uint32_t>(b), g_last_gx, g_last_gy});
|
injection.push_mkb(MkbEvent{Mkb_MouseUp, static_cast<std::uint32_t>(b), g_last_gx, g_last_gy});
|
||||||
g_mouse_down[b] = false;
|
g_mouse_down[b] = false;
|
||||||
}
|
}
|
||||||
@@ -126,8 +162,7 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring
|
|||||||
// own desktop use isn't injected. If we can't forward for ANY reason -- subsystem off, we lost
|
// own desktop use isn't injected. If we can't forward for ANY reason -- subsystem off, we lost
|
||||||
// focus, or the game is gone -- release everything we're still holding first, so a key/button
|
// focus, or the game is gone -- release everything we're still holding first, so a key/button
|
||||||
// held at that moment doesn't stick down in the guest.
|
// held at that moment doesn't stick down in the guest.
|
||||||
if (!injection.mkb_enabled() || GetForegroundWindow() != host_hwnd || game == nullptr || !IsWindow(game))
|
if (!injection.mkb_enabled() || GetForegroundWindow() != host_hwnd || game == nullptr || !IsWindow(game)) {
|
||||||
{
|
|
||||||
release_held_keys(injection);
|
release_held_keys(injection);
|
||||||
release_held_mouse(injection);
|
release_held_mouse(injection);
|
||||||
return;
|
return;
|
||||||
@@ -136,35 +171,26 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring
|
|||||||
ImGuiIO& io = ImGui::GetIO();
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
|
||||||
// --- Keyboard (unless ImGui is using it for e.g. a text field -- then release what we hold) ---
|
// --- Keyboard (unless ImGui is using it for e.g. a text field -- then release what we hold) ---
|
||||||
if (io.WantCaptureKeyboard)
|
if (io.WantCaptureKeyboard) {
|
||||||
{
|
|
||||||
release_held_keys(injection);
|
release_held_keys(injection);
|
||||||
}
|
} else {
|
||||||
else
|
for (ImGuiKey k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; k = static_cast<ImGuiKey>(k + 1)) {
|
||||||
{
|
|
||||||
for (ImGuiKey k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; k = static_cast<ImGuiKey>(k + 1))
|
|
||||||
{
|
|
||||||
const int vk = imgui_key_to_vk(k);
|
const int vk = imgui_key_to_vk(k);
|
||||||
if (vk == 0)
|
if (vk == 0) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (ImGui::IsKeyPressed(k, false))
|
if (ImGui::IsKeyPressed(k, false)) {
|
||||||
{
|
|
||||||
injection.push_mkb(MkbEvent{Mkb_KeyDown, static_cast<std::uint32_t>(vk), 0, 0});
|
injection.push_mkb(MkbEvent{Mkb_KeyDown, static_cast<std::uint32_t>(vk), 0, 0});
|
||||||
g_key_down[vk & 0xFF] = true;
|
g_key_down[vk & 0xFF] = true;
|
||||||
}
|
}
|
||||||
if (ImGui::IsKeyReleased(k))
|
if (ImGui::IsKeyReleased(k)) {
|
||||||
{
|
|
||||||
injection.push_mkb(MkbEvent{Mkb_KeyUp, static_cast<std::uint32_t>(vk), 0, 0});
|
injection.push_mkb(MkbEvent{Mkb_KeyUp, static_cast<std::uint32_t>(vk), 0, 0});
|
||||||
g_key_down[vk & 0xFF] = false;
|
g_key_down[vk & 0xFF] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (int i = 0; i < io.InputQueueCharacters.Size; ++i)
|
for (int i = 0; i < io.InputQueueCharacters.Size; ++i) {
|
||||||
{
|
|
||||||
const ImWchar c = io.InputQueueCharacters[i];
|
const ImWchar c = io.InputQueueCharacters[i];
|
||||||
if (c != 0)
|
if (c != 0) {
|
||||||
{
|
|
||||||
injection.push_mkb(MkbEvent{Mkb_Char, static_cast<std::uint32_t>(c), 0, 0});
|
injection.push_mkb(MkbEvent{Mkb_Char, static_cast<std::uint32_t>(c), 0, 0});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,8 +199,7 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring
|
|||||||
// --- Mouse (clicks + wheel only, and only while mirroring and ImGui isn't using the mouse) ---
|
// --- Mouse (clicks + wheel only, and only while mirroring and ImGui isn't using the mouse) ---
|
||||||
// When we're not forwarding the mouse, still release any button we hold (below), so it can't stick.
|
// When we're not forwarding the mouse, still release any button we hold (below), so it can't stick.
|
||||||
const bool forwarding_mouse = mirroring && !io.WantCaptureMouse;
|
const bool forwarding_mouse = mirroring && !io.WantCaptureMouse;
|
||||||
if (!forwarding_mouse)
|
if (!forwarding_mouse) {
|
||||||
{
|
|
||||||
release_held_mouse(injection);
|
release_held_mouse(injection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -187,15 +212,12 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring
|
|||||||
m.host_y = static_cast<int>(io.MousePos.y);
|
m.host_y = static_cast<int>(io.MousePos.y);
|
||||||
m.dst_w = host_client.right;
|
m.dst_w = host_client.right;
|
||||||
m.dst_h = host_client.bottom;
|
m.dst_h = host_client.bottom;
|
||||||
if (source_hooked)
|
if (source_hooked) {
|
||||||
{
|
|
||||||
// Hooked capture mirrors the backbuffer (client area), no decorations.
|
// Hooked capture mirrors the backbuffer (client area), no decorations.
|
||||||
const VideoShareView v = injection.video_share();
|
const VideoShareView v = injection.video_share();
|
||||||
m.src_w = m.client_w = static_cast<int>(v.width);
|
m.src_w = m.client_w = static_cast<int>(v.width);
|
||||||
m.src_h = m.client_h = static_cast<int>(v.height);
|
m.src_h = m.client_h = static_cast<int>(v.height);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
// WGC captures the whole window; the client area sits at a decoration offset.
|
// WGC captures the whole window; the client area sits at a decoration offset.
|
||||||
RECT wr{}, cr{};
|
RECT wr{}, cr{};
|
||||||
POINT client_origin{0, 0};
|
POINT client_origin{0, 0};
|
||||||
@@ -212,8 +234,7 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring
|
|||||||
|
|
||||||
int gx = 0, gy = 0;
|
int gx = 0, gy = 0;
|
||||||
const bool on_game = map_host_to_game_client(m, gx, gy);
|
const bool on_game = map_host_to_game_client(m, gx, gy);
|
||||||
if (on_game)
|
if (on_game) {
|
||||||
{
|
|
||||||
g_last_gx = gx;
|
g_last_gx = gx;
|
||||||
g_last_gy = gy;
|
g_last_gy = gy;
|
||||||
}
|
}
|
||||||
@@ -222,8 +243,7 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring
|
|||||||
|
|
||||||
for (int button = 0; button < 3; ++button) // 0=left, 1=right, 2=middle
|
for (int button = 0; button < 3; ++button) // 0=left, 1=right, 2=middle
|
||||||
{
|
{
|
||||||
if (on_game && ImGui::IsMouseClicked(button))
|
if (on_game && ImGui::IsMouseClicked(button)) {
|
||||||
{
|
|
||||||
injection.push_mkb(MkbEvent{Mkb_MouseDown, static_cast<std::uint32_t>(button), mx, my});
|
injection.push_mkb(MkbEvent{Mkb_MouseDown, static_cast<std::uint32_t>(button), mx, my});
|
||||||
g_mouse_down[button] = true;
|
g_mouse_down[button] = true;
|
||||||
}
|
}
|
||||||
@@ -233,8 +253,7 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring
|
|||||||
g_mouse_down[button] = false;
|
g_mouse_down[button] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (on_game && io.MouseWheel != 0.0f)
|
if (on_game && io.MouseWheel != 0.0f) {
|
||||||
{
|
|
||||||
const int delta = static_cast<int>(io.MouseWheel * WHEEL_DELTA);
|
const int delta = static_cast<int>(io.MouseWheel * WHEEL_DELTA);
|
||||||
injection.push_mkb(MkbEvent{Mkb_Wheel, static_cast<std::uint32_t>(delta), mx, my});
|
injection.push_mkb(MkbEvent{Mkb_Wheel, static_cast<std::uint32_t>(delta), mx, my});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
class InjectionPanel;
|
class InjectionPanel;
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,9 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
struct MkbMapInput
|
struct MkbMapInput {
|
||||||
{
|
|
||||||
int host_x = 0, host_y = 0; // mouse in host-window client pixels
|
int host_x = 0, host_y = 0; // mouse in host-window client pixels
|
||||||
int dst_w = 0, dst_h = 0; // host window client size
|
int dst_w = 0, dst_h = 0; // host window client size
|
||||||
int src_w = 0, src_h = 0; // captured frame size (WGC=window, Hooked=backbuffer)
|
int src_w = 0, src_h = 0; // captured frame size (WGC=window, Hooked=backbuffer)
|
||||||
@@ -28,28 +26,24 @@ struct MkbMapInput
|
|||||||
// client area; false if it falls on a letterbox bar or the window decorations.
|
// client area; false if it falls on a letterbox bar or the window decorations.
|
||||||
inline bool map_host_to_game_client(const MkbMapInput& in, int& gx, int& gy)
|
inline bool map_host_to_game_client(const MkbMapInput& in, int& gx, int& gy)
|
||||||
{
|
{
|
||||||
if (in.src_w <= 0 || in.src_h <= 0 || in.dst_w <= 0 || in.dst_h <= 0 || in.client_w <= 0 || in.client_h <= 0)
|
if (in.src_w <= 0 || in.src_h <= 0 || in.dst_w <= 0 || in.dst_h <= 0 || in.client_w <= 0 || in.client_h <= 0) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invert the letterbox: the frame is fit (aspect-preserved) and centered in dst.
|
// Invert the letterbox: the frame is fit (aspect-preserved) and centered in dst.
|
||||||
const double scale =
|
const double scale = std::min(static_cast<double>(in.dst_w) / in.src_w, static_cast<double>(in.dst_h) / in.src_h);
|
||||||
std::min(static_cast<double>(in.dst_w) / in.src_w, static_cast<double>(in.dst_h) / in.src_h);
|
|
||||||
const double ox = (in.dst_w - in.src_w * scale) * 0.5;
|
const double ox = (in.dst_w - in.src_w * scale) * 0.5;
|
||||||
const double oy = (in.dst_h - in.src_h * scale) * 0.5;
|
const double oy = (in.dst_h - in.src_h * scale) * 0.5;
|
||||||
|
|
||||||
const double fx = (in.host_x - ox) / scale; // position in captured-frame pixels
|
const double fx = (in.host_x - ox) / scale; // position in captured-frame pixels
|
||||||
const double fy = (in.host_y - oy) / scale;
|
const double fy = (in.host_y - oy) / scale;
|
||||||
if (fx < 0.0 || fy < 0.0 || fx >= in.src_w || fy >= in.src_h)
|
if (fx < 0.0 || fy < 0.0 || fx >= in.src_w || fy >= in.src_h) {
|
||||||
{
|
|
||||||
return false; // on a letterbox bar
|
return false; // on a letterbox bar
|
||||||
}
|
}
|
||||||
|
|
||||||
const double cx = fx - in.client_off_x; // into client space
|
const double cx = fx - in.client_off_x; // into client space
|
||||||
const double cy = fy - in.client_off_y;
|
const double cy = fy - in.client_off_y;
|
||||||
if (cx < 0.0 || cy < 0.0 || cx >= in.client_w || cy >= in.client_h)
|
if (cx < 0.0 || cy < 0.0 || cx >= in.client_w || cy >= in.client_h) {
|
||||||
{
|
|
||||||
return false; // on the window decorations
|
return false; // on the window decorations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,27 +5,22 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <tlhelp32.h>
|
#include <tlhelp32.h>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
std::vector<ProcessEntry> list_processes()
|
std::vector<ProcessEntry> list_processes()
|
||||||
{
|
{
|
||||||
std::vector<ProcessEntry> result;
|
std::vector<ProcessEntry> result;
|
||||||
|
|
||||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||||
if (snapshot == INVALID_HANDLE_VALUE)
|
if (snapshot == INVALID_HANDLE_VALUE) {
|
||||||
{
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
PROCESSENTRY32W entry = {};
|
PROCESSENTRY32W entry = {};
|
||||||
entry.dwSize = sizeof(entry);
|
entry.dwSize = sizeof(entry);
|
||||||
if (Process32FirstW(snapshot, &entry))
|
if (Process32FirstW(snapshot, &entry)) {
|
||||||
{
|
do {
|
||||||
do
|
if (entry.th32ProcessID == 0) {
|
||||||
{
|
|
||||||
if (entry.th32ProcessID == 0)
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
result.push_back(ProcessEntry{entry.th32ProcessID, entry.szExeFile});
|
result.push_back(ProcessEntry{entry.th32ProcessID, entry.szExeFile});
|
||||||
|
|||||||
@@ -4,11 +4,9 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
struct ProcessEntry
|
struct ProcessEntry {
|
||||||
{
|
|
||||||
unsigned long pid = 0;
|
unsigned long pid = 0;
|
||||||
std::wstring exe_name; // image base name, e.g. "game.exe"
|
std::wstring exe_name; // image base name, e.g. "game.exe"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,14 +7,11 @@
|
|||||||
|
|
||||||
#include "inject/process_list.hpp"
|
#include "inject/process_list.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
struct EnumCtx
|
struct EnumCtx {
|
||||||
{
|
|
||||||
std::vector<WindowEntry>* out;
|
std::vector<WindowEntry>* out;
|
||||||
const std::unordered_map<unsigned long, std::wstring>* names;
|
const std::unordered_map<unsigned long, std::wstring>* names;
|
||||||
DWORD self_pid;
|
DWORD self_pid;
|
||||||
@@ -25,23 +22,19 @@ BOOL CALLBACK enum_proc(HWND hwnd, LPARAM lparam)
|
|||||||
auto* ctx = reinterpret_cast<EnumCtx*>(lparam);
|
auto* ctx = reinterpret_cast<EnumCtx*>(lparam);
|
||||||
|
|
||||||
// Keep only "alt-tab" windows: visible, titled, root-owner, non-tool, not ours.
|
// Keep only "alt-tab" windows: visible, titled, root-owner, non-tool, not ours.
|
||||||
if (!IsWindowVisible(hwnd) || GetAncestor(hwnd, GA_ROOTOWNER) != hwnd)
|
if (!IsWindowVisible(hwnd) || GetAncestor(hwnd, GA_ROOTOWNER) != hwnd) {
|
||||||
{
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
const int len = GetWindowTextLengthW(hwnd);
|
const int len = GetWindowTextLengthW(hwnd);
|
||||||
if (len <= 0)
|
if (len <= 0) {
|
||||||
{
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
if ((GetWindowLongW(hwnd, GWL_EXSTYLE) & WS_EX_TOOLWINDOW) != 0)
|
if ((GetWindowLongW(hwnd, GWL_EXSTYLE) & WS_EX_TOOLWINDOW) != 0) {
|
||||||
{
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
DWORD pid = 0;
|
DWORD pid = 0;
|
||||||
GetWindowThreadProcessId(hwnd, &pid);
|
GetWindowThreadProcessId(hwnd, &pid);
|
||||||
if (pid == 0 || pid == ctx->self_pid)
|
if (pid == 0 || pid == ctx->self_pid) {
|
||||||
{
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +42,7 @@ BOOL CALLBACK enum_proc(HWND hwnd, LPARAM lparam)
|
|||||||
GetWindowTextW(hwnd, title.data(), len + 1);
|
GetWindowTextW(hwnd, title.data(), len + 1);
|
||||||
|
|
||||||
std::wstring exe;
|
std::wstring exe;
|
||||||
if (const auto it = ctx->names->find(pid); it != ctx->names->end())
|
if (const auto it = ctx->names->find(pid); it != ctx->names->end()) {
|
||||||
{
|
|
||||||
exe = it->second;
|
exe = it->second;
|
||||||
}
|
}
|
||||||
ctx->out->push_back(WindowEntry{pid, hwnd, std::move(title), std::move(exe)});
|
ctx->out->push_back(WindowEntry{pid, hwnd, std::move(title), std::move(exe)});
|
||||||
@@ -64,8 +56,7 @@ std::vector<WindowEntry> list_windows()
|
|||||||
// pid -> image name, so each window can show its owning process without a separate
|
// pid -> image name, so each window can show its owning process without a separate
|
||||||
// OpenProcess per window.
|
// OpenProcess per window.
|
||||||
std::unordered_map<unsigned long, std::wstring> names;
|
std::unordered_map<unsigned long, std::wstring> names;
|
||||||
for (const ProcessEntry& p : list_processes())
|
for (const ProcessEntry& p : list_processes()) {
|
||||||
{
|
|
||||||
names.emplace(p.pid, p.exe_name);
|
names.emplace(p.pid, p.exe_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,9 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
struct WindowEntry
|
struct WindowEntry {
|
||||||
{
|
|
||||||
unsigned long pid = 0; // owning process id
|
unsigned long pid = 0; // owning process id
|
||||||
void* hwnd = nullptr; // HWND (opaque here to keep windows.h out of the header)
|
void* hwnd = nullptr; // HWND (opaque here to keep windows.h out of the header)
|
||||||
std::wstring title; // window caption
|
std::wstring title; // window caption
|
||||||
|
|||||||
@@ -12,11 +12,9 @@
|
|||||||
#include "ui/text_match.hpp"
|
#include "ui/text_match.hpp"
|
||||||
#include "util/utf8.hpp"
|
#include "util/utf8.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f);
|
const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f);
|
||||||
const ImVec4 kRed(1.0f, 0.45f, 0.4f, 1.0f);
|
const ImVec4 kRed(1.0f, 0.45f, 0.4f, 1.0f);
|
||||||
@@ -40,8 +38,7 @@ std::wstring hook_dll_path()
|
|||||||
const DWORD len = GetModuleFileNameW(nullptr, buffer, MAX_PATH);
|
const DWORD len = GetModuleFileNameW(nullptr, buffer, MAX_PATH);
|
||||||
std::wstring path(buffer, len);
|
std::wstring path(buffer, len);
|
||||||
const std::size_t slash = path.find_last_of(L"\\/");
|
const std::size_t slash = path.find_last_of(L"\\/");
|
||||||
if (slash != std::wstring::npos)
|
if (slash != std::wstring::npos) {
|
||||||
{
|
|
||||||
path.resize(slash + 1);
|
path.resize(slash + 1);
|
||||||
}
|
}
|
||||||
path += L"coop_hook.dll";
|
path += L"coop_hook.dll";
|
||||||
@@ -67,8 +64,7 @@ InjectionPanel::~InjectionPanel()
|
|||||||
// stays injected, dormant). Short timeout -- the flags persist in the section the DLL keeps
|
// stays injected, dormant). Short timeout -- the flags persist in the section the DLL keeps
|
||||||
// alive, so the unhook completes even if the process exits before it confirms.
|
// alive, so the unhook completes even if the process exits before it confirms.
|
||||||
disconnect_graceful(/*timeout_ms=*/300);
|
disconnect_graceful(/*timeout_ms=*/300);
|
||||||
if (vk_layer_enabled_)
|
if (vk_layer_enabled_) {
|
||||||
{
|
|
||||||
unregister_vk_layer(); // don't leave the implicit layer registered after the tool closes
|
unregister_vk_layer(); // don't leave the implicit layer registered after the tool closes
|
||||||
}
|
}
|
||||||
close_target_handle();
|
close_target_handle();
|
||||||
@@ -80,11 +76,9 @@ void InjectionPanel::disconnect_graceful(int timeout_ms)
|
|||||||
// (bounded) for it to confirm before we drop the channel. The flags persist in the section the
|
// (bounded) for it to confirm before we drop the channel. The flags persist in the section the
|
||||||
// DLL keeps alive, so it unhooks even if we time out or exit first -- the wait just lets us
|
// DLL keeps alive, so it unhooks even if we time out or exit first -- the wait just lets us
|
||||||
// observe a clean game. The DLL is left injected (dormant) for a later reconnect; we never eject.
|
// observe a clean game. The DLL is left injected (dormant) for a later reconnect; we never eject.
|
||||||
if (server_.running())
|
if (server_.running()) {
|
||||||
{
|
|
||||||
server_.request_unhook_all();
|
server_.request_unhook_all();
|
||||||
for (int waited = 0; waited < timeout_ms && !server_.all_hooks_removed(); waited += 10)
|
for (int waited = 0; waited < timeout_ms && !server_.all_hooks_removed(); waited += 10) {
|
||||||
{
|
|
||||||
Sleep(10);
|
Sleep(10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,8 +89,7 @@ void InjectionPanel::disconnect_graceful(int timeout_ms)
|
|||||||
|
|
||||||
void InjectionPanel::close_target_handle()
|
void InjectionPanel::close_target_handle()
|
||||||
{
|
{
|
||||||
if (target_process_ != nullptr)
|
if (target_process_ != nullptr) {
|
||||||
{
|
|
||||||
CloseHandle(target_process_);
|
CloseHandle(target_process_);
|
||||||
target_process_ = nullptr;
|
target_process_ = nullptr;
|
||||||
}
|
}
|
||||||
@@ -114,23 +107,19 @@ void InjectionPanel::tick()
|
|||||||
|
|
||||||
void InjectionPanel::auto_reattach_tick()
|
void InjectionPanel::auto_reattach_tick()
|
||||||
{
|
{
|
||||||
if (!auto_reattach_ || target_state_ != TargetState::Terminated || selected_name_.empty())
|
if (!auto_reattach_ || target_state_ != TargetState::Terminated || selected_name_.empty()) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Poll the process list a couple of times a second (cheap, and we want to catch the
|
// Poll the process list a couple of times a second (cheap, and we want to catch the
|
||||||
// relaunch early to read the exact audio format before the game creates its client).
|
// relaunch early to read the exact audio format before the game creates its client).
|
||||||
const double now = ImGui::GetTime();
|
const double now = ImGui::GetTime();
|
||||||
if (now - last_auto_poll_ < 0.5)
|
if (now - last_auto_poll_ < 0.5) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
last_auto_poll_ = now;
|
last_auto_poll_ = now;
|
||||||
refresh_processes();
|
refresh_processes();
|
||||||
for (const ProcessEntry& e : processes_)
|
for (const ProcessEntry& e : processes_) {
|
||||||
{
|
if (iequals_name(e.exe_name, selected_name_)) {
|
||||||
if (iequals_name(e.exe_name, selected_name_))
|
|
||||||
{
|
|
||||||
// The same game relaunched -> tear down the stale channel and re-attach to it.
|
// The same game relaunched -> tear down the stale channel and re-attach to it.
|
||||||
server_.stop();
|
server_.stop();
|
||||||
close_target_handle();
|
close_target_handle();
|
||||||
@@ -144,8 +133,7 @@ void InjectionPanel::auto_reattach_tick()
|
|||||||
|
|
||||||
void InjectionPanel::update_liveness()
|
void InjectionPanel::update_liveness()
|
||||||
{
|
{
|
||||||
if (!injected_)
|
if (!injected_) {
|
||||||
{
|
|
||||||
target_state_ = TargetState::NotInjected;
|
target_state_ = TargetState::NotInjected;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -153,8 +141,7 @@ void InjectionPanel::update_liveness()
|
|||||||
// Process gone? The handle was opened with SYNCHRONIZE at inject time, so a
|
// Process gone? The handle was opened with SYNCHRONIZE at inject time, so a
|
||||||
// signaled wait means it exited. This is authoritative even if the heartbeat
|
// signaled wait means it exited. This is authoritative even if the heartbeat
|
||||||
// happened to look alive a moment ago.
|
// happened to look alive a moment ago.
|
||||||
if (target_process_ != nullptr && WaitForSingleObject(target_process_, 0) == WAIT_OBJECT_0)
|
if (target_process_ != nullptr && WaitForSingleObject(target_process_, 0) == WAIT_OBJECT_0) {
|
||||||
{
|
|
||||||
target_state_ = TargetState::Terminated;
|
target_state_ = TargetState::Terminated;
|
||||||
dll_alive_ = false;
|
dll_alive_ = false;
|
||||||
return;
|
return;
|
||||||
@@ -164,14 +151,11 @@ void InjectionPanel::update_liveness()
|
|||||||
// process whose heartbeat stalled for ~2 s is frozen, not gone -- a distinct state.
|
// process whose heartbeat stalled for ~2 s is frozen, not gone -- a distinct state.
|
||||||
const std::uint32_t hb = server_.hook_status().heartbeat;
|
const std::uint32_t hb = server_.hook_status().heartbeat;
|
||||||
const double now = ImGui::GetTime();
|
const double now = ImGui::GetTime();
|
||||||
if (hb != last_heartbeat_)
|
if (hb != last_heartbeat_) {
|
||||||
{
|
|
||||||
last_heartbeat_ = hb;
|
last_heartbeat_ = hb;
|
||||||
last_heartbeat_time_ = now;
|
last_heartbeat_time_ = now;
|
||||||
dll_alive_ = true;
|
dll_alive_ = true;
|
||||||
}
|
} else if (now - last_heartbeat_time_ > 2.0) {
|
||||||
else if (now - last_heartbeat_time_ > 2.0)
|
|
||||||
{
|
|
||||||
dll_alive_ = false;
|
dll_alive_ = false;
|
||||||
}
|
}
|
||||||
target_state_ = dll_alive_ ? TargetState::Alive : TargetState::Hung;
|
target_state_ = dll_alive_ ? TargetState::Alive : TargetState::Hung;
|
||||||
@@ -211,23 +195,21 @@ void InjectionPanel::reconnect_selected()
|
|||||||
// Re-attach to a DLL that's already injected and alive (a prior session left it dormant after a
|
// Re-attach to a DLL that's already injected and alive (a prior session left it dormant after a
|
||||||
// graceful disconnect, or the tool restarted): bring the channel back up on the SAME per-pid
|
// graceful disconnect, or the tool restarted): bring the channel back up on the SAME per-pid
|
||||||
// section the DLL still holds and re-publish the desired subsystem state -- no re-injection.
|
// section the DLL still holds and re-publish the desired subsystem state -- no re-injection.
|
||||||
if (!server_.start(selected_pid_))
|
if (!server_.start(selected_pid_)) {
|
||||||
{
|
|
||||||
status_ = "Failed to re-attach shared memory.";
|
status_ = "Failed to re-attach shared memory.";
|
||||||
status_color_ = kRed;
|
status_color_ = kRed;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
publish_subsystem_state();
|
publish_subsystem_state();
|
||||||
begin_liveness_tracking();
|
begin_liveness_tracking();
|
||||||
status_ = "Reconnected to " + narrow(selected_name_) + " (pid " + std::to_string(selected_pid_) +
|
status_ = "Reconnected to " + narrow(selected_name_) + " (pid " + std::to_string(selected_pid_)
|
||||||
") -- reused the injected DLL.";
|
+ ") -- reused the injected DLL.";
|
||||||
status_color_ = kGreen;
|
status_color_ = kGreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InjectionPanel::inject_selected()
|
void InjectionPanel::inject_selected()
|
||||||
{
|
{
|
||||||
if (selected_pid_ == 0)
|
if (selected_pid_ == 0) {
|
||||||
{
|
|
||||||
status_ = "Select a target process first.";
|
status_ = "Select a target process first.";
|
||||||
status_color_ = kRed;
|
status_color_ = kRed;
|
||||||
return;
|
return;
|
||||||
@@ -236,16 +218,14 @@ void InjectionPanel::inject_selected()
|
|||||||
// If our DLL is already injected and alive in this target (left dormant by a graceful disconnect,
|
// If our DLL is already injected and alive in this target (left dormant by a graceful disconnect,
|
||||||
// or surviving a tool restart -- it keeps the section alive), reconnect to it instead of
|
// or surviving a tool restart -- it keeps the section alive), reconnect to it instead of
|
||||||
// injecting a second time.
|
// injecting a second time.
|
||||||
if (hook_dll_alive(selected_pid_))
|
if (hook_dll_alive(selected_pid_)) {
|
||||||
{
|
|
||||||
reconnect_selected();
|
reconnect_selected();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bring up the shared-memory channel before injecting so the hook finds it
|
// Bring up the shared-memory channel before injecting so the hook finds it
|
||||||
// immediately on load.
|
// immediately on load.
|
||||||
if (!server_.start(selected_pid_))
|
if (!server_.start(selected_pid_)) {
|
||||||
{
|
|
||||||
status_ = "Failed to create shared memory.";
|
status_ = "Failed to create shared memory.";
|
||||||
status_color_ = kRed;
|
status_color_ = kRed;
|
||||||
return;
|
return;
|
||||||
@@ -254,18 +234,14 @@ void InjectionPanel::inject_selected()
|
|||||||
publish_subsystem_state();
|
publish_subsystem_state();
|
||||||
|
|
||||||
const InjectResult result = inject_dll(selected_pid_, hook_dll_path());
|
const InjectResult result = inject_dll(selected_pid_, hook_dll_path());
|
||||||
if (result.status == InjectStatus::Ok)
|
if (result.status == InjectStatus::Ok) {
|
||||||
{
|
|
||||||
begin_liveness_tracking();
|
begin_liveness_tracking();
|
||||||
status_ = "Injected into " + narrow(selected_name_) + " (pid " + std::to_string(selected_pid_) + ").";
|
status_ = "Injected into " + narrow(selected_name_) + " (pid " + std::to_string(selected_pid_) + ").";
|
||||||
status_color_ = kGreen;
|
status_color_ = kGreen;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
server_.stop();
|
server_.stop();
|
||||||
status_ = std::string("Injection failed: ") + to_string(result.status);
|
status_ = std::string("Injection failed: ") + to_string(result.status);
|
||||||
if (result.os_error != 0)
|
if (result.os_error != 0) {
|
||||||
{
|
|
||||||
status_ += " [err " + std::to_string(result.os_error) + "]";
|
status_ += " [err " + std::to_string(result.os_error) + "]";
|
||||||
}
|
}
|
||||||
status_color_ = kRed;
|
status_color_ = kRed;
|
||||||
@@ -274,31 +250,26 @@ void InjectionPanel::inject_selected()
|
|||||||
|
|
||||||
void InjectionPanel::reattach()
|
void InjectionPanel::reattach()
|
||||||
{
|
{
|
||||||
if (selected_name_.empty())
|
if (selected_name_.empty()) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find live processes that share the original target's image name.
|
// Find live processes that share the original target's image name.
|
||||||
refresh_processes();
|
refresh_processes();
|
||||||
std::vector<unsigned long> matches;
|
std::vector<unsigned long> matches;
|
||||||
for (const ProcessEntry& e : processes_)
|
for (const ProcessEntry& e : processes_) {
|
||||||
{
|
if (iequals_name(e.exe_name, selected_name_)) {
|
||||||
if (iequals_name(e.exe_name, selected_name_))
|
|
||||||
{
|
|
||||||
matches.push_back(e.pid);
|
matches.push_back(e.pid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string name = narrow(selected_name_);
|
const std::string name = narrow(selected_name_);
|
||||||
if (matches.empty())
|
if (matches.empty()) {
|
||||||
{
|
|
||||||
status_ = "No running \"" + name + "\" to re-attach to.";
|
status_ = "No running \"" + name + "\" to re-attach to.";
|
||||||
status_color_ = kRed;
|
status_color_ = kRed;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (matches.size() > 1)
|
if (matches.size() > 1) {
|
||||||
{
|
|
||||||
// Don't guess which instance: filter the picker to the matches so the operator
|
// Don't guess which instance: filter the picker to the matches so the operator
|
||||||
// chooses, then injects via the normal button.
|
// chooses, then injects via the normal button.
|
||||||
snprintf(filter_, sizeof(filter_), "%s", name.c_str());
|
snprintf(filter_, sizeof(filter_), "%s", name.c_str());
|
||||||
@@ -320,10 +291,8 @@ void InjectionPanel::reattach()
|
|||||||
unsigned long InjectionPanel::dev_inject_by_name(const std::wstring& image_name)
|
unsigned long InjectionPanel::dev_inject_by_name(const std::wstring& image_name)
|
||||||
{
|
{
|
||||||
refresh_processes();
|
refresh_processes();
|
||||||
for (const ProcessEntry& e : processes_)
|
for (const ProcessEntry& e : processes_) {
|
||||||
{
|
if (iequals_name(e.exe_name, image_name)) {
|
||||||
if (iequals_name(e.exe_name, image_name))
|
|
||||||
{
|
|
||||||
selected_pid_ = e.pid;
|
selected_pid_ = e.pid;
|
||||||
selected_name_ = e.exe_name;
|
selected_name_ = e.exe_name;
|
||||||
inject_selected();
|
inject_selected();
|
||||||
@@ -336,8 +305,7 @@ unsigned long InjectionPanel::dev_inject_by_name(const std::wstring& image_name)
|
|||||||
|
|
||||||
void InjectionPanel::publish(const std::array<PadInfo, kMaxPads>& pads)
|
void InjectionPanel::publish(const std::array<PadInfo, kMaxPads>& pads)
|
||||||
{
|
{
|
||||||
if (!test_input_.load(std::memory_order_relaxed))
|
if (!test_input_.load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
server_.publish(pads);
|
server_.publish(pads);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -356,8 +324,7 @@ void InjectionPanel::publish(const std::array<PadInfo, kMaxPads>& pads)
|
|||||||
pad.state.packet = static_cast<std::uint32_t>(ms);
|
pad.state.packet = static_cast<std::uint32_t>(ms);
|
||||||
pad.state.thumb_lx = static_cast<std::int16_t>(std::cos(t) * 30000.0);
|
pad.state.thumb_lx = static_cast<std::int16_t>(std::cos(t) * 30000.0);
|
||||||
pad.state.thumb_ly = static_cast<std::int16_t>(std::sin(t) * 30000.0);
|
pad.state.thumb_ly = static_cast<std::int16_t>(std::sin(t) * 30000.0);
|
||||||
if ((ms / 1000) % 2 == 0)
|
if ((ms / 1000) % 2 == 0) {
|
||||||
{
|
|
||||||
pad.state.buttons |= 0x1000; // XINPUT_GAMEPAD_A
|
pad.state.buttons |= 0x1000; // XINPUT_GAMEPAD_A
|
||||||
}
|
}
|
||||||
server_.publish(synthetic);
|
server_.publish(synthetic);
|
||||||
@@ -368,35 +335,28 @@ void InjectionPanel::draw_hook_list(const HookStatusView& status)
|
|||||||
static const char* kSubsysName[] = {"Input", "Focus", "Audio", "Video", "MKB"};
|
static const char* kSubsysName[] = {"Input", "Focus", "Audio", "Video", "MKB"};
|
||||||
|
|
||||||
const std::uint32_t n = status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries;
|
const std::uint32_t n = status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries;
|
||||||
if (n == 0)
|
if (n == 0) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ImGui::CollapsingHeader("Installed hooks", ImGuiTreeNodeFlags_DefaultOpen))
|
if (!ImGui::CollapsingHeader("Installed hooks", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ImGui::BeginTable("hooks", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp))
|
if (ImGui::BeginTable("hooks", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) {
|
||||||
{
|
|
||||||
ImGui::TableSetupColumn("Hook");
|
ImGui::TableSetupColumn("Hook");
|
||||||
ImGui::TableSetupColumn("On", ImGuiTableColumnFlags_WidthFixed);
|
ImGui::TableSetupColumn("On", ImGuiTableColumnFlags_WidthFixed);
|
||||||
ImGui::TableSetupColumn("Calls", ImGuiTableColumnFlags_WidthFixed);
|
ImGui::TableSetupColumn("Calls", ImGuiTableColumnFlags_WidthFixed);
|
||||||
ImGui::TableHeadersRow();
|
ImGui::TableHeadersRow();
|
||||||
|
|
||||||
// Group rows by subsystem so related hooks sit together.
|
// Group rows by subsystem so related hooks sit together.
|
||||||
for (std::uint32_t sub = 0; sub < HookSubsys_Count; ++sub)
|
for (std::uint32_t sub = 0; sub < HookSubsys_Count; ++sub) {
|
||||||
{
|
|
||||||
bool header_done = false;
|
bool header_done = false;
|
||||||
for (std::uint32_t i = 0; i < n; ++i)
|
for (std::uint32_t i = 0; i < n; ++i) {
|
||||||
{
|
|
||||||
const HookEntry& e = status.hook_entries[i];
|
const HookEntry& e = status.hook_entries[i];
|
||||||
if (e.subsystem != sub)
|
if (e.subsystem != sub) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!header_done)
|
if (!header_done) {
|
||||||
{
|
|
||||||
ImGui::TableNextRow();
|
ImGui::TableNextRow();
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
ImGui::TextDisabled("%s", kSubsysName[sub < HookSubsys_Count ? sub : 0]);
|
ImGui::TextDisabled("%s", kSubsysName[sub < HookSubsys_Count ? sub : 0]);
|
||||||
@@ -408,12 +368,9 @@ void InjectionPanel::draw_hook_list(const HookStatusView& status)
|
|||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
ImGui::TextUnformatted(e.name);
|
ImGui::TextUnformatted(e.name);
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
if (e.installed)
|
if (e.installed) {
|
||||||
{
|
|
||||||
ImGui::TextColored(kGreen, "yes");
|
ImGui::TextColored(kGreen, "yes");
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextDisabled("no");
|
ImGui::TextDisabled("no");
|
||||||
}
|
}
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
@@ -428,10 +385,8 @@ void InjectionPanel::draw_hook_list(const HookStatusView& status)
|
|||||||
static bool subsystem_installed(const HookStatusView& status, std::uint32_t subsystem)
|
static bool subsystem_installed(const HookStatusView& status, std::uint32_t subsystem)
|
||||||
{
|
{
|
||||||
const std::uint32_t n = status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries;
|
const std::uint32_t n = status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries;
|
||||||
for (std::uint32_t i = 0; i < n; ++i)
|
for (std::uint32_t i = 0; i < n; ++i) {
|
||||||
{
|
if (status.hook_entries[i].subsystem == subsystem && status.hook_entries[i].installed) {
|
||||||
if (status.hook_entries[i].subsystem == subsystem && status.hook_entries[i].installed)
|
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,8 +397,7 @@ void InjectionPanel::draw_subsystem_controls(const HookStatusView& status)
|
|||||||
{
|
{
|
||||||
ImGui::SeparatorText("Subsystems (hook / unhook)");
|
ImGui::SeparatorText("Subsystems (hook / unhook)");
|
||||||
|
|
||||||
struct Row
|
struct Row {
|
||||||
{
|
|
||||||
const char* label;
|
const char* label;
|
||||||
std::uint32_t subsystem;
|
std::uint32_t subsystem;
|
||||||
bool* want;
|
bool* want;
|
||||||
@@ -457,25 +411,19 @@ void InjectionPanel::draw_subsystem_controls(const HookStatusView& status)
|
|||||||
{"Mouse + keyboard forwarding", HookSubsys_Mkb, &want_mkb_, "clicks/keys reach the game"},
|
{"Mouse + keyboard forwarding", HookSubsys_Mkb, &want_mkb_, "clicks/keys reach the game"},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const Row& r : rows)
|
for (const Row& r : rows) {
|
||||||
{
|
|
||||||
ImGui::PushID(r.label);
|
ImGui::PushID(r.label);
|
||||||
if (ImGui::Checkbox(r.label, r.want))
|
if (ImGui::Checkbox(r.label, r.want)) {
|
||||||
{
|
|
||||||
server_.set_subsystem_enabled(r.subsystem, *r.want);
|
server_.set_subsystem_enabled(r.subsystem, *r.want);
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
const bool on = subsystem_installed(status, r.subsystem);
|
const bool on = subsystem_installed(status, r.subsystem);
|
||||||
if (*r.want != on)
|
if (*r.want != on) {
|
||||||
{
|
|
||||||
ImGui::TextColored(kGrey, "(%s...)", *r.want ? "installing" : "removing");
|
ImGui::TextColored(kGrey, "(%s...)", *r.want ? "installing" : "removing");
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextColored(on ? kGreen : kGrey, on ? "installed" : "off");
|
ImGui::TextColored(on ? kGreen : kGrey, on ? "installed" : "off");
|
||||||
}
|
}
|
||||||
if (!*r.want)
|
if (!*r.want) {
|
||||||
{
|
|
||||||
ImGui::TextDisabled(" off: %s won't work", r.depends);
|
ImGui::TextDisabled(" off: %s won't work", r.depends);
|
||||||
}
|
}
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
@@ -483,33 +431,28 @@ void InjectionPanel::draw_subsystem_controls(const HookStatusView& status)
|
|||||||
|
|
||||||
// Cursor release is a Focus sub-option for games that clip/recenter the mouse,
|
// Cursor release is a Focus sub-option for games that clip/recenter the mouse,
|
||||||
// which would otherwise trap the operator.
|
// which would otherwise trap the operator.
|
||||||
if (ImGui::Checkbox("Release operator cursor (free the game's clip) [F2]", &release_cursor_))
|
if (ImGui::Checkbox("Release operator cursor (free the game's clip) [F2]", &release_cursor_)) {
|
||||||
{
|
|
||||||
server_.set_cursor_clip_allowed(!release_cursor_);
|
server_.set_cursor_clip_allowed(!release_cursor_);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void InjectionPanel::draw_hook_status(bool debug_details)
|
void InjectionPanel::draw_hook_status(bool debug_details)
|
||||||
{
|
{
|
||||||
if (!server_.running())
|
if (!server_.running()) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HookStatusView status = server_.hook_status();
|
const HookStatusView status = server_.hook_status();
|
||||||
|
|
||||||
ImGui::SeparatorText("Hook status");
|
ImGui::SeparatorText("Hook status");
|
||||||
if (!injected_)
|
if (!injected_) {
|
||||||
{
|
|
||||||
ImGui::TextColored(kGrey, "Not injected.");
|
ImGui::TextColored(kGrey, "Not injected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (target_state_)
|
switch (target_state_) {
|
||||||
{
|
|
||||||
case TargetState::Alive:
|
case TargetState::Alive:
|
||||||
ImGui::TextColored(kGreen, "Hook DLL loaded in pid %lu (heartbeat %u)", server_.target_pid(),
|
ImGui::TextColored(kGreen, "Hook DLL loaded in pid %lu (heartbeat %u)", server_.target_pid(), status.heartbeat);
|
||||||
status.heartbeat);
|
|
||||||
break;
|
break;
|
||||||
case TargetState::Hung:
|
case TargetState::Hung:
|
||||||
ImGui::TextColored(kRed, "Target not responding -- heartbeat stalled (frozen?).");
|
ImGui::TextColored(kRed, "Target not responding -- heartbeat stalled (frozen?).");
|
||||||
@@ -525,16 +468,14 @@ void InjectionPanel::draw_hook_status(bool debug_details)
|
|||||||
ImGui::BeginDisabled(target_state_ != TargetState::Alive);
|
ImGui::BeginDisabled(target_state_ != TargetState::Alive);
|
||||||
draw_subsystem_controls(status);
|
draw_subsystem_controls(status);
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
if (target_state_ != TargetState::Alive)
|
if (target_state_ != TargetState::Alive) {
|
||||||
{
|
|
||||||
ImGui::TextDisabled("(connect to a live game to change these)"); // why the toggles are locked
|
ImGui::TextDisabled("(connect to a live game to change these)"); // why the toggles are locked
|
||||||
}
|
}
|
||||||
ImGui::TextDisabled("Controller poll rates are in the Controllers panel.");
|
ImGui::TextDisabled("Controller poll rates are in the Controllers panel.");
|
||||||
|
|
||||||
draw_hook_list(status);
|
draw_hook_list(status);
|
||||||
|
|
||||||
if (!debug_details)
|
if (!debug_details) {
|
||||||
{
|
|
||||||
return; // everything below is diagnostic detail
|
return; // everything below is diagnostic detail
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,17 +489,12 @@ void InjectionPanel::draw_hook_status(bool debug_details)
|
|||||||
// Input-path diagnostics: a focus-gated detection path would explain a game
|
// Input-path diagnostics: a focus-gated detection path would explain a game
|
||||||
// that only accepts the controller when it has true focus.
|
// that only accepts the controller when it has true focus.
|
||||||
ImGui::SeparatorText("Input path");
|
ImGui::SeparatorText("Input path");
|
||||||
if (status.raw_input_gamepad)
|
if (status.raw_input_gamepad) {
|
||||||
{
|
|
||||||
ImGui::TextColored(status.raw_input_gamepad_sink ? kGreen : kRed, "Raw Input gamepad: yes (INPUTSINK %s)",
|
ImGui::TextColored(status.raw_input_gamepad_sink ? kGreen : kRed, "Raw Input gamepad: yes (INPUTSINK %s)",
|
||||||
status.raw_input_gamepad_sink ? "set -> bg ok" : "MISSING -> focus-gated!");
|
status.raw_input_gamepad_sink ? "set -> bg ok" : "MISSING -> focus-gated!");
|
||||||
}
|
} else if (status.raw_input_registered) {
|
||||||
else if (status.raw_input_registered)
|
|
||||||
{
|
|
||||||
ImGui::TextColored(kGrey, "Raw Input: registered, but not for a gamepad usage");
|
ImGui::TextColored(kGrey, "Raw Input: registered, but not for a gamepad usage");
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextColored(kGrey, "Raw Input: not registered");
|
ImGui::TextColored(kGrey, "Raw Input: not registered");
|
||||||
}
|
}
|
||||||
ImGui::TextColored(status.dinput_loaded ? kRed : kGrey, "DirectInput dll loaded: %s",
|
ImGui::TextColored(status.dinput_loaded ? kRed : kGrey, "DirectInput dll loaded: %s",
|
||||||
@@ -570,22 +506,15 @@ void InjectionPanel::draw(bool debug_details)
|
|||||||
apply_panel_layout(Panel::Injection);
|
apply_panel_layout(Panel::Injection);
|
||||||
ImGui::Begin("Injection");
|
ImGui::Begin("Injection");
|
||||||
|
|
||||||
if (server_.running())
|
if (server_.running()) {
|
||||||
{
|
if (target_state_ == TargetState::Terminated) {
|
||||||
if (target_state_ == TargetState::Terminated)
|
|
||||||
{
|
|
||||||
ImGui::TextColored(kRed, "Target (pid %lu) has terminated.", server_.target_pid());
|
ImGui::TextColored(kRed, "Target (pid %lu) has terminated.", server_.target_pid());
|
||||||
}
|
} else if (target_state_ == TargetState::Hung) {
|
||||||
else if (target_state_ == TargetState::Hung)
|
|
||||||
{
|
|
||||||
ImGui::TextColored(kRed, "Target (pid %lu) is not responding.", server_.target_pid());
|
ImGui::TextColored(kRed, "Target (pid %lu) is not responding.", server_.target_pid());
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextColored(kGreen, "Connected to pid %lu", server_.target_pid());
|
ImGui::TextColored(kGreen, "Connected to pid %lu", server_.target_pid());
|
||||||
}
|
}
|
||||||
if (ImGui::Button("Disconnect"))
|
if (ImGui::Button("Disconnect")) {
|
||||||
{
|
|
||||||
// Leave the game vanilla: unhook everything before dropping the channel. The DLL stays
|
// Leave the game vanilla: unhook everything before dropping the channel. The DLL stays
|
||||||
// injected (dormant), so it can be reconnected later without re-injecting.
|
// injected (dormant), so it can be reconnected later without re-injecting.
|
||||||
disconnect_graceful(/*timeout_ms=*/700);
|
disconnect_graceful(/*timeout_ms=*/700);
|
||||||
@@ -594,11 +523,9 @@ void InjectionPanel::draw(bool debug_details)
|
|||||||
}
|
}
|
||||||
// A relaunched game has a new pid; re-attach by image name without hunting for
|
// A relaunched game has a new pid; re-attach by image name without hunting for
|
||||||
// it in the list. Only offered once the old target is gone.
|
// it in the list. Only offered once the old target is gone.
|
||||||
if (target_state_ == TargetState::Terminated && !selected_name_.empty())
|
if (target_state_ == TargetState::Terminated && !selected_name_.empty()) {
|
||||||
{
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Button("Re-attach"))
|
if (ImGui::Button("Re-attach")) {
|
||||||
{
|
|
||||||
reattach();
|
reattach();
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
@@ -609,32 +536,25 @@ void InjectionPanel::draw(bool debug_details)
|
|||||||
|
|
||||||
// Session options for the selected game, shown whether or not we're connected -- so they can be set
|
// Session options for the selected game, shown whether or not we're connected -- so they can be set
|
||||||
// up before launching the game, and an enabled auto re-attach is never hidden after a disconnect.
|
// up before launching the game, and an enabled auto re-attach is never hidden after a disconnect.
|
||||||
if (!selected_name_.empty())
|
if (!selected_name_.empty()) {
|
||||||
{
|
|
||||||
// Session-only auto re-attach: tick it, then kill + relaunch the game and it re-injects itself
|
// Session-only auto re-attach: tick it, then kill + relaunch the game and it re-injects itself
|
||||||
// early -- the kill+relaunch fix for a wrong audio format, without picking a target again.
|
// early -- the kill+relaunch fix for a wrong audio format, without picking a target again.
|
||||||
ImGui::Checkbox("Auto re-attach this game on relaunch", &auto_reattach_);
|
ImGui::Checkbox("Auto re-attach this game on relaunch", &auto_reattach_);
|
||||||
if (auto_reattach_ && target_state_ == TargetState::Terminated)
|
if (auto_reattach_ && target_state_ == TargetState::Terminated) {
|
||||||
{
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextColored(kGrey, "(watching for %s...)", narrow(selected_name_).c_str());
|
ImGui::TextColored(kGrey, "(watching for %s...)", narrow(selected_name_).c_str());
|
||||||
}
|
}
|
||||||
// Opt-in Vulkan capture layer: for Vulkan games that initialize Vulkan immediately (where even
|
// Opt-in Vulkan capture layer: for Vulkan games that initialize Vulkan immediately (where even
|
||||||
// auto-attach injects too late -- see the red banner), register a per-user implicit layer scoped
|
// auto-attach injects too late -- see the red banner), register a per-user implicit layer scoped
|
||||||
// to this game so the next launch is captured from the first frame. Removed when unticked / exit.
|
// to this game so the next launch is captured from the first frame. Removed when unticked / exit.
|
||||||
if (ImGui::Checkbox("Set up Vulkan layer (for immediate-init Vulkan games)", &vk_layer_enabled_))
|
if (ImGui::Checkbox("Set up Vulkan layer (for immediate-init Vulkan games)", &vk_layer_enabled_)) {
|
||||||
{
|
if (vk_layer_enabled_) {
|
||||||
if (vk_layer_enabled_)
|
|
||||||
{
|
|
||||||
vk_layer_enabled_ = register_vk_layer(selected_name_);
|
vk_layer_enabled_ = register_vk_layer(selected_name_);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
unregister_vk_layer();
|
unregister_vk_layer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered())
|
if (ImGui::IsItemHovered()) {
|
||||||
{
|
|
||||||
ImGui::SetTooltip("Registers a per-user (HKCU, no admin) implicit Vulkan layer scoped to\n"
|
ImGui::SetTooltip("Registers a per-user (HKCU, no admin) implicit Vulkan layer scoped to\n"
|
||||||
"this game, so a relaunch is captured before Vulkan init. Pair with\n"
|
"this game, so a relaunch is captured before Vulkan init. Pair with\n"
|
||||||
"Auto re-attach. Removed when you untick it or close the tool.");
|
"Auto re-attach. Removed when you untick it or close the tool.");
|
||||||
@@ -642,28 +562,23 @@ void InjectionPanel::draw(bool debug_details)
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TextUnformatted("Target window");
|
ImGui::TextUnformatted("Target window");
|
||||||
if (ImGui::Button("Refresh"))
|
if (ImGui::Button("Refresh")) {
|
||||||
{
|
|
||||||
refresh_targets();
|
refresh_targets();
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::SetNextItemWidth(-1.0f);
|
ImGui::SetNextItemWidth(-1.0f);
|
||||||
ImGui::InputTextWithHint("##wfilter", "filter by title or process...", window_filter_, sizeof(window_filter_));
|
ImGui::InputTextWithHint("##wfilter", "filter by title or process...", window_filter_, sizeof(window_filter_));
|
||||||
|
|
||||||
if (ImGui::BeginListBox("##windows", ImVec2(-1.0f, 180.0f)))
|
if (ImGui::BeginListBox("##windows", ImVec2(-1.0f, 180.0f))) {
|
||||||
{
|
for (const WindowEntry& w : windows_) {
|
||||||
for (const WindowEntry& w : windows_)
|
if (!contains_ci_w(w.title, window_filter_) && !contains_ci_w(w.exe_name, window_filter_)) {
|
||||||
{
|
|
||||||
if (!contains_ci_w(w.title, window_filter_) && !contains_ci_w(w.exe_name, window_filter_))
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const bool selected = w.pid == selected_pid_;
|
const bool selected = w.pid == selected_pid_;
|
||||||
char label[400];
|
char label[400];
|
||||||
snprintf(label, sizeof(label), "%-32s [%s %lu]", narrow(w.title).c_str(),
|
snprintf(label, sizeof(label), "%-32s [%s %lu]", narrow(w.title).c_str(), narrow(w.exe_name).c_str(),
|
||||||
narrow(w.exe_name).c_str(), w.pid);
|
w.pid);
|
||||||
if (ImGui::Selectable(label, selected))
|
if (ImGui::Selectable(label, selected)) {
|
||||||
{
|
|
||||||
selected_pid_ = w.pid;
|
selected_pid_ = w.pid;
|
||||||
selected_name_ = w.exe_name;
|
selected_name_ = w.exe_name;
|
||||||
}
|
}
|
||||||
@@ -673,24 +588,19 @@ void InjectionPanel::draw(bool debug_details)
|
|||||||
|
|
||||||
// The full process list is the advanced fallback (e.g. a windowless game host),
|
// The full process list is the advanced fallback (e.g. a windowless game host),
|
||||||
// kept out of the way unless the operator wants it.
|
// kept out of the way unless the operator wants it.
|
||||||
if (debug_details)
|
if (debug_details) {
|
||||||
{
|
|
||||||
ImGui::SeparatorText("All processes (advanced)");
|
ImGui::SeparatorText("All processes (advanced)");
|
||||||
ImGui::SetNextItemWidth(-1.0f);
|
ImGui::SetNextItemWidth(-1.0f);
|
||||||
ImGui::InputTextWithHint("##filter", "filter by name...", filter_, sizeof(filter_));
|
ImGui::InputTextWithHint("##filter", "filter by name...", filter_, sizeof(filter_));
|
||||||
if (ImGui::BeginListBox("##processes", ImVec2(-1.0f, 160.0f)))
|
if (ImGui::BeginListBox("##processes", ImVec2(-1.0f, 160.0f))) {
|
||||||
{
|
for (const ProcessEntry& entry : processes_) {
|
||||||
for (const ProcessEntry& entry : processes_)
|
if (!contains_ci_w(entry.exe_name, filter_)) {
|
||||||
{
|
|
||||||
if (!contains_ci_w(entry.exe_name, filter_))
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const bool selected = entry.pid == selected_pid_;
|
const bool selected = entry.pid == selected_pid_;
|
||||||
char label[300];
|
char label[300];
|
||||||
snprintf(label, sizeof(label), "%-40s %lu", narrow(entry.exe_name).c_str(), entry.pid);
|
snprintf(label, sizeof(label), "%-40s %lu", narrow(entry.exe_name).c_str(), entry.pid);
|
||||||
if (ImGui::Selectable(label, selected))
|
if (ImGui::Selectable(label, selected)) {
|
||||||
{
|
|
||||||
selected_pid_ = entry.pid;
|
selected_pid_ = entry.pid;
|
||||||
selected_name_ = entry.exe_name;
|
selected_name_ = entry.exe_name;
|
||||||
}
|
}
|
||||||
@@ -701,20 +611,17 @@ void InjectionPanel::draw(bool debug_details)
|
|||||||
|
|
||||||
const bool can_inject = selected_pid_ != 0;
|
const bool can_inject = selected_pid_ != 0;
|
||||||
ImGui::BeginDisabled(!can_inject);
|
ImGui::BeginDisabled(!can_inject);
|
||||||
if (ImGui::Button("Inject & Connect", ImVec2(-1.0f, 0.0f)))
|
if (ImGui::Button("Inject & Connect", ImVec2(-1.0f, 0.0f))) {
|
||||||
{
|
|
||||||
inject_selected();
|
inject_selected();
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
// Explain the disabled state on hover (AllowWhenDisabled, since the button is greyed out).
|
// Explain the disabled state on hover (AllowWhenDisabled, since the button is greyed out).
|
||||||
if (!can_inject && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
if (!can_inject && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
||||||
{
|
|
||||||
ImGui::SetTooltip("Pick a target window or process above first.\n"
|
ImGui::SetTooltip("Pick a target window or process above first.\n"
|
||||||
"If the game already has the DLL (e.g. after a reconnect), this reuses it.");
|
"If the game already has the DLL (e.g. after a reconnect), this reuses it.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!status_.empty())
|
if (!status_.empty()) {
|
||||||
{
|
|
||||||
ImGui::TextColored(status_color_, "%s", status_.c_str());
|
ImGui::TextColored(status_color_, "%s", status_.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,20 +15,17 @@
|
|||||||
#include "inject/window_list.hpp"
|
#include "inject/window_list.hpp"
|
||||||
#include "ipc/ipc_server.hpp"
|
#include "ipc/ipc_server.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// Liveness of the injected target, surfaced in the UI so a dead/hung game is obvious.
|
// Liveness of the injected target, surfaced in the UI so a dead/hung game is obvious.
|
||||||
enum class TargetState
|
enum class TargetState {
|
||||||
{
|
|
||||||
NotInjected, // no hook loaded
|
NotInjected, // no hook loaded
|
||||||
Alive, // process running and the hook heartbeat is advancing
|
Alive, // process running and the hook heartbeat is advancing
|
||||||
Hung, // process still exists but the heartbeat stalled (not responding)
|
Hung, // process still exists but the heartbeat stalled (not responding)
|
||||||
Terminated, // process has exited
|
Terminated, // process has exited
|
||||||
};
|
};
|
||||||
|
|
||||||
class InjectionPanel
|
class InjectionPanel {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
InjectionPanel();
|
InjectionPanel();
|
||||||
~InjectionPanel();
|
~InjectionPanel();
|
||||||
@@ -49,10 +46,7 @@ public:
|
|||||||
// Test harness (debug builds only): inject into the first running process whose image
|
// Test harness (debug builds only): inject into the first running process whose image
|
||||||
// name matches. Returns the pid on success, 0 otherwise. Same path as the UI button.
|
// name matches. Returns the pid on success, 0 otherwise. Same path as the UI button.
|
||||||
unsigned long dev_inject_by_name(const std::wstring& image_name);
|
unsigned long dev_inject_by_name(const std::wstring& image_name);
|
||||||
void dev_set_auto_reattach(bool on)
|
void dev_set_auto_reattach(bool on) { auto_reattach_ = on; }
|
||||||
{
|
|
||||||
auto_reattach_ = on;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Forward the latest pad snapshot to the injected hook (if connected). When
|
// Forward the latest pad snapshot to the injected hook (if connected). When
|
||||||
@@ -61,35 +55,25 @@ public:
|
|||||||
|
|
||||||
// Enable/disable synthetic test input. The toggle itself lives in the Controllers
|
// Enable/disable synthetic test input. The toggle itself lives in the Controllers
|
||||||
// panel (a controller-debug aid); the host feeds its state here each frame.
|
// panel (a controller-debug aid); the host feeds its state here each frame.
|
||||||
void set_test_input(bool on)
|
void set_test_input(bool on) { test_input_.store(on, std::memory_order_relaxed); }
|
||||||
{
|
|
||||||
test_input_.store(on, std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The injected game's main window, as reported by the hook (null if none). A
|
// The injected game's main window, as reported by the hook (null if none). A
|
||||||
// terminated target's HWND is stale/invalid, so report none -- the capture and
|
// terminated target's HWND is stale/invalid, so report none -- the capture and
|
||||||
// audio panels then drop to idle instead of chasing a dead window.
|
// audio panels then drop to idle instead of chasing a dead window.
|
||||||
[[nodiscard]] HWND game_hwnd() const
|
[[nodiscard]] HWND game_hwnd() const
|
||||||
{
|
{
|
||||||
if (target_state_ == TargetState::Terminated)
|
if (target_state_ == TargetState::Terminated) {
|
||||||
{
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return reinterpret_cast<HWND>(server_.hook_status().game_hwnd);
|
return reinterpret_cast<HWND>(server_.hook_status().game_hwnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current liveness of the injected target (for other panels / status).
|
// Current liveness of the injected target (for other panels / status).
|
||||||
[[nodiscard]] TargetState target_state() const
|
[[nodiscard]] TargetState target_state() const { return target_state_; }
|
||||||
{
|
|
||||||
return target_state_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The hook's full diagnostics back-channel (other panels read the audio
|
// The hook's full diagnostics back-channel (other panels read the audio
|
||||||
// render-stream counts from here).
|
// render-stream counts from here).
|
||||||
[[nodiscard]] HookStatusView hook_status() const
|
[[nodiscard]] HookStatusView hook_status() const { return server_.hook_status(); }
|
||||||
{
|
|
||||||
return server_.hook_status();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drain log lines the hook streamed (for the Log window). No-op if not active.
|
// Drain log lines the hook streamed (for the Log window). No-op if not active.
|
||||||
template <typename F>
|
template <typename F>
|
||||||
@@ -100,23 +84,14 @@ public:
|
|||||||
|
|
||||||
// Emit a host-side line into the Log window (color-coded by level), e.g. an
|
// Emit a host-side line into the Log window (color-coded by level), e.g. an
|
||||||
// override-overwrite warning. No-op if not connected.
|
// override-overwrite warning. No-op if not connected.
|
||||||
void host_log(std::uint32_t level, const char* text)
|
void host_log(std::uint32_t level, const char* text) { server_.host_log(level, text); }
|
||||||
{
|
|
||||||
server_.host_log(level, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Present-hook video path (consumed by the Video mirror panel) ----------
|
// --- Present-hook video path (consumed by the Video mirror panel) ----------
|
||||||
|
|
||||||
[[nodiscard]] unsigned long target_pid() const
|
[[nodiscard]] unsigned long target_pid() const { return server_.target_pid(); }
|
||||||
{
|
|
||||||
return server_.target_pid();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The hook's Present-hook video channel snapshot (shared-texture descriptor).
|
// The hook's Present-hook video channel snapshot (shared-texture descriptor).
|
||||||
[[nodiscard]] VideoShareView video_share() const
|
[[nodiscard]] VideoShareView video_share() const { return server_.video_share(); }
|
||||||
{
|
|
||||||
return server_.video_share();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request the Present-hook video subsystem be installed/removed. Keeps the
|
// Request the Present-hook video subsystem be installed/removed. Keeps the
|
||||||
// Injection panel's own checkbox in sync, so the Video panel can drive it.
|
// Injection panel's own checkbox in sync, so the Video panel can drive it.
|
||||||
@@ -126,25 +101,16 @@ public:
|
|||||||
server_.set_subsystem_enabled(HookSubsys_Video, on);
|
server_.set_subsystem_enabled(HookSubsys_Video, on);
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool video_requested() const
|
[[nodiscard]] bool video_requested() const { return want_video_; }
|
||||||
{
|
|
||||||
return want_video_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Mouse + keyboard forwarding -------------------------------------------
|
// --- Mouse + keyboard forwarding -------------------------------------------
|
||||||
|
|
||||||
// Whether the operator enabled the MKB-forwarding subsystem (the toggle is the
|
// Whether the operator enabled the MKB-forwarding subsystem (the toggle is the
|
||||||
// hook). The host's MKB forwarder only runs while this is on and a hook is alive.
|
// hook). The host's MKB forwarder only runs while this is on and a hook is alive.
|
||||||
[[nodiscard]] bool mkb_enabled() const
|
[[nodiscard]] bool mkb_enabled() const { return want_mkb_ && injected_; }
|
||||||
{
|
|
||||||
return want_mkb_ && injected_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enqueue an MKB event for the hook to forward into the game.
|
// Enqueue an MKB event for the hook to forward into the game.
|
||||||
void push_mkb(const MkbEvent& ev)
|
void push_mkb(const MkbEvent& ev) { server_.push_mkb(ev); }
|
||||||
{
|
|
||||||
server_.push_mkb(ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Cursor release (for cursor-clipping games) ----------------------------
|
// --- Cursor release (for cursor-clipping games) ----------------------------
|
||||||
|
|
||||||
@@ -156,10 +122,7 @@ public:
|
|||||||
server_.set_cursor_clip_allowed(!release_cursor_);
|
server_.set_cursor_clip_allowed(!release_cursor_);
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool cursor_released() const
|
[[nodiscard]] bool cursor_released() const { return release_cursor_; }
|
||||||
{
|
|
||||||
return release_cursor_;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refresh_targets(); // refresh both the window list and the process list
|
void refresh_targets(); // refresh both the window list and the process list
|
||||||
|
|||||||
@@ -12,11 +12,9 @@
|
|||||||
|
|
||||||
#include "coop/protocol.hpp"
|
#include "coop/protocol.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
struct PadInfo
|
struct PadInfo {
|
||||||
{
|
|
||||||
bool connected = false;
|
bool connected = false;
|
||||||
CoopPadState state = {};
|
CoopPadState state = {};
|
||||||
std::string source; // human-readable label for the debug overlay
|
std::string source; // human-readable label for the debug overlay
|
||||||
@@ -25,15 +23,13 @@ struct PadInfo
|
|||||||
// A copy-safe snapshot of the input backend's state, published by the input worker
|
// A copy-safe snapshot of the input backend's state, published by the input worker
|
||||||
// thread for the UI to display. This decouples the Controllers panel (UI thread) from
|
// thread for the UI to display. This decouples the Controllers panel (UI thread) from
|
||||||
// the worker's live polling, so the worker can own its InputSource exclusively.
|
// the worker's live polling, so the worker can own its InputSource exclusively.
|
||||||
struct InputSnapshot
|
struct InputSnapshot {
|
||||||
{
|
|
||||||
std::array<PadInfo, kMaxPads> pads{};
|
std::array<PadInfo, kMaxPads> pads{};
|
||||||
const char* backend = "XInput"; // backend name (static string literal; thread-safe to share)
|
const char* backend = "XInput"; // backend name (static string literal; thread-safe to share)
|
||||||
bool steam_active = false;
|
bool steam_active = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class InputSource
|
class InputSource {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
virtual ~InputSource() = default;
|
virtual ~InputSource() = default;
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
#include "input/steam_input_source.hpp"
|
#include "input/steam_input_source.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
InputWorker::~InputWorker()
|
InputWorker::~InputWorker()
|
||||||
{
|
{
|
||||||
@@ -21,8 +20,7 @@ InputWorker::~InputWorker()
|
|||||||
|
|
||||||
void InputWorker::start(InjectionPanel* injection, std::string steam_manifest)
|
void InputWorker::start(InjectionPanel* injection, std::string steam_manifest)
|
||||||
{
|
{
|
||||||
if (running_.load(std::memory_order_acquire))
|
if (running_.load(std::memory_order_acquire)) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
injection_ = injection;
|
injection_ = injection;
|
||||||
@@ -34,8 +32,7 @@ void InputWorker::start(InjectionPanel* injection, std::string steam_manifest)
|
|||||||
void InputWorker::stop()
|
void InputWorker::stop()
|
||||||
{
|
{
|
||||||
running_.store(false, std::memory_order_release);
|
running_.store(false, std::memory_order_release);
|
||||||
if (thread_.joinable())
|
if (thread_.joinable()) {
|
||||||
{
|
|
||||||
thread_.join();
|
thread_.join();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,36 +65,28 @@ void InputWorker::run()
|
|||||||
std::uint16_t last_rumble_l[kMaxPads] = {};
|
std::uint16_t last_rumble_l[kMaxPads] = {};
|
||||||
std::uint16_t last_rumble_r[kMaxPads] = {};
|
std::uint16_t last_rumble_r[kMaxPads] = {};
|
||||||
|
|
||||||
while (running_.load(std::memory_order_relaxed))
|
while (running_.load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
const bool want_steam = want_steam_.load(std::memory_order_relaxed);
|
const bool want_steam = want_steam_.load(std::memory_order_relaxed);
|
||||||
#ifdef COOP_WITH_STEAM
|
#ifdef COOP_WITH_STEAM
|
||||||
// Reconcile the backend with the UI's request. Initializing Steam Input hijacks
|
// Reconcile the backend with the UI's request. Initializing Steam Input hijacks
|
||||||
// XInput, so it's strictly opt-in; a failed init falls back to plain XInput and
|
// XInput, so it's strictly opt-in; a failed init falls back to plain XInput and
|
||||||
// flags steam_failed_ so the UI can reset its toggle (and a later retry is
|
// flags steam_failed_ so the UI can reset its toggle (and a later retry is
|
||||||
// possible once the request is cleared).
|
// possible once the request is cleared).
|
||||||
if (want_steam && steam == nullptr && !steam_failed_.load(std::memory_order_relaxed))
|
if (want_steam && steam == nullptr && !steam_failed_.load(std::memory_order_relaxed)) {
|
||||||
{
|
|
||||||
steam = std::make_unique<SteamInputSource>();
|
steam = std::make_unique<SteamInputSource>();
|
||||||
if (steam->init(steam_manifest_))
|
if (steam->init(steam_manifest_)) {
|
||||||
{
|
|
||||||
active = steam.get();
|
active = steam.get();
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
steam.reset();
|
steam.reset();
|
||||||
active = &xinput;
|
active = &xinput;
|
||||||
steam_failed_.store(true, std::memory_order_relaxed);
|
steam_failed_.store(true, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
} else if (!want_steam && steam != nullptr) {
|
||||||
else if (!want_steam && steam != nullptr)
|
|
||||||
{
|
|
||||||
steam->shutdown();
|
steam->shutdown();
|
||||||
steam.reset();
|
steam.reset();
|
||||||
active = &xinput;
|
active = &xinput;
|
||||||
}
|
}
|
||||||
if (!want_steam)
|
if (!want_steam) {
|
||||||
{
|
|
||||||
steam_failed_.store(false, std::memory_order_relaxed); // allow a future retry
|
steam_failed_.store(false, std::memory_order_relaxed); // allow a future retry
|
||||||
}
|
}
|
||||||
const bool steam_active = steam != nullptr;
|
const bool steam_active = steam != nullptr;
|
||||||
@@ -108,17 +97,14 @@ void InputWorker::run()
|
|||||||
|
|
||||||
active->poll();
|
active->poll();
|
||||||
|
|
||||||
if (injection_ != nullptr)
|
if (injection_ != nullptr) {
|
||||||
{
|
|
||||||
// Push the latest pads to the game (publish() substitutes synthetic test input
|
// Push the latest pads to the game (publish() substitutes synthetic test input
|
||||||
// itself when that mode is on), then forward any newly requested rumble.
|
// itself when that mode is on), then forward any newly requested rumble.
|
||||||
injection_->publish(active->pads());
|
injection_->publish(active->pads());
|
||||||
|
|
||||||
const HookStatusView hs = injection_->hook_status();
|
const HookStatusView hs = injection_->hook_status();
|
||||||
for (int i = 0; i < static_cast<int>(kMaxPads); ++i)
|
for (int i = 0; i < static_cast<int>(kMaxPads); ++i) {
|
||||||
{
|
if (hs.rumble_left[i] != last_rumble_l[i] || hs.rumble_right[i] != last_rumble_r[i]) {
|
||||||
if (hs.rumble_left[i] != last_rumble_l[i] || hs.rumble_right[i] != last_rumble_r[i])
|
|
||||||
{
|
|
||||||
active->set_rumble(i, hs.rumble_left[i], hs.rumble_right[i]);
|
active->set_rumble(i, hs.rumble_left[i], hs.rumble_right[i]);
|
||||||
last_rumble_l[i] = hs.rumble_left[i];
|
last_rumble_l[i] = hs.rumble_left[i];
|
||||||
last_rumble_r[i] = hs.rumble_right[i];
|
last_rumble_r[i] = hs.rumble_right[i];
|
||||||
@@ -134,8 +120,7 @@ void InputWorker::run()
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef COOP_WITH_STEAM
|
#ifdef COOP_WITH_STEAM
|
||||||
if (steam != nullptr)
|
if (steam != nullptr) {
|
||||||
{
|
|
||||||
steam->shutdown();
|
steam->shutdown();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -13,13 +13,11 @@
|
|||||||
|
|
||||||
#include "input/input_source.hpp"
|
#include "input/input_source.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
class InjectionPanel;
|
class InjectionPanel;
|
||||||
|
|
||||||
class InputWorker
|
class InputWorker {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
InputWorker() = default;
|
InputWorker() = default;
|
||||||
~InputWorker();
|
~InputWorker();
|
||||||
@@ -35,20 +33,14 @@ public:
|
|||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
// UI -> worker: request the Steam Input backend (true) or plain XInput (false).
|
// UI -> worker: request the Steam Input backend (true) or plain XInput (false).
|
||||||
void set_want_steam(bool on)
|
void set_want_steam(bool on) { want_steam_.store(on, std::memory_order_relaxed); }
|
||||||
{
|
|
||||||
want_steam_.store(on, std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// worker -> UI: latest snapshot for the Controllers panel (thread-safe copy).
|
// worker -> UI: latest snapshot for the Controllers panel (thread-safe copy).
|
||||||
[[nodiscard]] InputSnapshot snapshot() const;
|
[[nodiscard]] InputSnapshot snapshot() const;
|
||||||
|
|
||||||
// worker -> UI: Steam Input was requested but failed to start (so the panel can
|
// worker -> UI: Steam Input was requested but failed to start (so the panel can
|
||||||
// reset its toggle and fall back to XInput). Cleared once Steam is not requested.
|
// reset its toggle and fall back to XInput). Cleared once Steam is not requested.
|
||||||
[[nodiscard]] bool steam_failed() const
|
[[nodiscard]] bool steam_failed() const { return steam_failed_.load(std::memory_order_relaxed); }
|
||||||
{
|
|
||||||
return steam_failed_.load(std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void run();
|
void run();
|
||||||
|
|||||||
@@ -7,16 +7,13 @@
|
|||||||
|
|
||||||
#include <steam/steam_api.h>
|
#include <steam/steam_api.h>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
// Digital actions in the manifest, paired with the XInput button bit they map to.
|
// Digital actions in the manifest, paired with the XInput button bit they map to.
|
||||||
// Names must match host/assets/steam_input_actions.vdf.
|
// Names must match host/assets/steam_input_actions.vdf.
|
||||||
struct ButtonAction
|
struct ButtonAction {
|
||||||
{
|
|
||||||
const char* action;
|
const char* action;
|
||||||
std::uint16_t xinput_bit;
|
std::uint16_t xinput_bit;
|
||||||
};
|
};
|
||||||
@@ -40,12 +37,10 @@ const ButtonAction kButtons[kSteamButtonActions] = {
|
|||||||
|
|
||||||
std::int16_t to_axis(float v)
|
std::int16_t to_axis(float v)
|
||||||
{
|
{
|
||||||
if (v > 1.0f)
|
if (v > 1.0f) {
|
||||||
{
|
|
||||||
v = 1.0f;
|
v = 1.0f;
|
||||||
}
|
}
|
||||||
if (v < -1.0f)
|
if (v < -1.0f) {
|
||||||
{
|
|
||||||
v = -1.0f;
|
v = -1.0f;
|
||||||
}
|
}
|
||||||
return static_cast<std::int16_t>(v * 32767.0f);
|
return static_cast<std::int16_t>(v * 32767.0f);
|
||||||
@@ -53,12 +48,10 @@ std::int16_t to_axis(float v)
|
|||||||
|
|
||||||
std::uint8_t to_trigger(float v)
|
std::uint8_t to_trigger(float v)
|
||||||
{
|
{
|
||||||
if (v > 1.0f)
|
if (v > 1.0f) {
|
||||||
{
|
|
||||||
v = 1.0f;
|
v = 1.0f;
|
||||||
}
|
}
|
||||||
if (v < 0.0f)
|
if (v < 0.0f) {
|
||||||
{
|
|
||||||
v = 0.0f;
|
v = 0.0f;
|
||||||
}
|
}
|
||||||
return static_cast<std::uint8_t>(v * 255.0f);
|
return static_cast<std::uint8_t>(v * 255.0f);
|
||||||
@@ -75,25 +68,21 @@ bool SteamInputSource::init(const std::string& manifest_absolute_path)
|
|||||||
{
|
{
|
||||||
// Running standalone (not launched by Steam) without a steam_appid.txt makes
|
// Running standalone (not launched by Steam) without a steam_appid.txt makes
|
||||||
// SteamAPI_Init fail; that's fine -- we degrade to XInput.
|
// SteamAPI_Init fail; that's fine -- we degrade to XInput.
|
||||||
if (!SteamAPI_Init())
|
if (!SteamAPI_Init()) {
|
||||||
{
|
|
||||||
std::printf("SteamInput: SteamAPI_Init failed (not under Steam?); using XInput.\n");
|
std::printf("SteamInput: SteamAPI_Init failed (not under Steam?); using XInput.\n");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (SteamInput() == nullptr)
|
if (SteamInput() == nullptr) {
|
||||||
{
|
|
||||||
std::printf("SteamInput: ISteamInput unavailable; using XInput.\n");
|
std::printf("SteamInput: ISteamInput unavailable; using XInput.\n");
|
||||||
SteamAPI_Shutdown();
|
SteamAPI_Shutdown();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Point Steam Input at our bundled action manifest so we don't depend on a
|
// Point Steam Input at our bundled action manifest so we don't depend on a
|
||||||
// partner-backend-registered config. Must be called before Init().
|
// partner-backend-registered config. Must be called before Init().
|
||||||
if (!manifest_absolute_path.empty())
|
if (!manifest_absolute_path.empty()) {
|
||||||
{
|
|
||||||
SteamInput()->SetInputActionManifestFilePath(manifest_absolute_path.c_str());
|
SteamInput()->SetInputActionManifestFilePath(manifest_absolute_path.c_str());
|
||||||
}
|
}
|
||||||
if (!SteamInput()->Init(/*bExplicitlyCallRunFrame=*/false))
|
if (!SteamInput()->Init(/*bExplicitlyCallRunFrame=*/false)) {
|
||||||
{
|
|
||||||
std::printf("SteamInput: ISteamInput::Init failed; using XInput.\n");
|
std::printf("SteamInput: ISteamInput::Init failed; using XInput.\n");
|
||||||
SteamAPI_Shutdown();
|
SteamAPI_Shutdown();
|
||||||
return false;
|
return false;
|
||||||
@@ -108,8 +97,7 @@ bool SteamInputSource::init(const std::string& manifest_absolute_path)
|
|||||||
|
|
||||||
void SteamInputSource::shutdown()
|
void SteamInputSource::shutdown()
|
||||||
{
|
{
|
||||||
if (steam_ready_)
|
if (steam_ready_) {
|
||||||
{
|
|
||||||
SteamInput()->Shutdown();
|
SteamInput()->Shutdown();
|
||||||
SteamAPI_Shutdown();
|
SteamAPI_Shutdown();
|
||||||
steam_ready_ = false;
|
steam_ready_ = false;
|
||||||
@@ -120,8 +108,7 @@ void SteamInputSource::shutdown()
|
|||||||
void SteamInputSource::resolve_handles()
|
void SteamInputSource::resolve_handles()
|
||||||
{
|
{
|
||||||
action_set_ = SteamInput()->GetActionSetHandle("GameControls");
|
action_set_ = SteamInput()->GetActionSetHandle("GameControls");
|
||||||
for (int i = 0; i < kSteamButtonActions; ++i)
|
for (int i = 0; i < kSteamButtonActions; ++i) {
|
||||||
{
|
|
||||||
button_handles_[i] = SteamInput()->GetDigitalActionHandle(kButtons[i].action);
|
button_handles_[i] = SteamInput()->GetDigitalActionHandle(kButtons[i].action);
|
||||||
}
|
}
|
||||||
left_stick_ = SteamInput()->GetAnalogActionHandle("LeftStick");
|
left_stick_ = SteamInput()->GetAnalogActionHandle("LeftStick");
|
||||||
@@ -138,12 +125,10 @@ bool SteamInputSource::read_steam_pad(std::uint64_t controller, PadInfo& out) co
|
|||||||
st.connected = 1;
|
st.connected = 1;
|
||||||
bool any_active = false;
|
bool any_active = false;
|
||||||
|
|
||||||
for (int i = 0; i < kSteamButtonActions; ++i)
|
for (int i = 0; i < kSteamButtonActions; ++i) {
|
||||||
{
|
|
||||||
const InputDigitalActionData_t d = SteamInput()->GetDigitalActionData(controller, button_handles_[i]);
|
const InputDigitalActionData_t d = SteamInput()->GetDigitalActionData(controller, button_handles_[i]);
|
||||||
any_active = any_active || d.bActive;
|
any_active = any_active || d.bActive;
|
||||||
if (d.bState)
|
if (d.bState) {
|
||||||
{
|
|
||||||
st.buttons |= kButtons[i].xinput_bit;
|
st.buttons |= kButtons[i].xinput_bit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,8 +148,7 @@ bool SteamInputSource::read_steam_pad(std::uint64_t controller, PadInfo& out) co
|
|||||||
|
|
||||||
// No action is bound/active (e.g. the controller isn't using our manifest) ->
|
// No action is bound/active (e.g. the controller isn't using our manifest) ->
|
||||||
// let the XInput fallback handle this slot instead of reporting an empty pad.
|
// let the XInput fallback handle this slot instead of reporting an empty pad.
|
||||||
if (!any_active)
|
if (!any_active) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,8 +166,7 @@ void SteamInputSource::poll()
|
|||||||
xinput_.poll();
|
xinput_.poll();
|
||||||
pads_ = xinput_.pads();
|
pads_ = xinput_.pads();
|
||||||
|
|
||||||
if (!steam_ready_)
|
if (!steam_ready_) {
|
||||||
{
|
|
||||||
steam_count_ = 0;
|
steam_count_ = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -192,17 +175,14 @@ void SteamInputSource::poll()
|
|||||||
InputHandle_t handles[STEAM_INPUT_MAX_COUNT] = {};
|
InputHandle_t handles[STEAM_INPUT_MAX_COUNT] = {};
|
||||||
steam_count_ = SteamInput()->GetConnectedControllers(handles);
|
steam_count_ = SteamInput()->GetConnectedControllers(handles);
|
||||||
|
|
||||||
for (std::uint32_t i = 0; i < kMaxPads; ++i)
|
for (std::uint32_t i = 0; i < kMaxPads; ++i) {
|
||||||
{
|
|
||||||
controllers_[i] = 0;
|
controllers_[i] = 0;
|
||||||
steam_slot_[i] = false;
|
steam_slot_[i] = false;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < steam_count_ && i < static_cast<int>(kMaxPads); ++i)
|
for (int i = 0; i < steam_count_ && i < static_cast<int>(kMaxPads); ++i) {
|
||||||
{
|
|
||||||
controllers_[i] = handles[i];
|
controllers_[i] = handles[i];
|
||||||
PadInfo steam_pad;
|
PadInfo steam_pad;
|
||||||
if (read_steam_pad(handles[i], steam_pad))
|
if (read_steam_pad(handles[i], steam_pad)) {
|
||||||
{
|
|
||||||
pads_[i] = steam_pad; // Steam controller active on this slot -> use it
|
pads_[i] = steam_pad; // Steam controller active on this slot -> use it
|
||||||
steam_slot_[i] = true;
|
steam_slot_[i] = true;
|
||||||
}
|
}
|
||||||
@@ -211,12 +191,10 @@ void SteamInputSource::poll()
|
|||||||
|
|
||||||
void SteamInputSource::set_rumble(int slot, std::uint16_t left, std::uint16_t right)
|
void SteamInputSource::set_rumble(int slot, std::uint16_t left, std::uint16_t right)
|
||||||
{
|
{
|
||||||
if (slot < 0 || slot >= static_cast<int>(kMaxPads))
|
if (slot < 0 || slot >= static_cast<int>(kMaxPads)) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (steam_ready_ && steam_slot_[slot] && controllers_[slot] != 0)
|
if (steam_ready_ && steam_slot_[slot] && controllers_[slot] != 0) {
|
||||||
{
|
|
||||||
SteamInput()->TriggerVibration(controllers_[slot], left, right);
|
SteamInput()->TriggerVibration(controllers_[slot], left, right);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,12 @@
|
|||||||
#include "input/input_source.hpp"
|
#include "input/input_source.hpp"
|
||||||
#include "input/xinput_source.hpp"
|
#include "input/xinput_source.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// Number of digital (button) actions in the bundled action manifest.
|
// Number of digital (button) actions in the bundled action manifest.
|
||||||
inline constexpr int kSteamButtonActions = 15;
|
inline constexpr int kSteamButtonActions = 15;
|
||||||
|
|
||||||
class SteamInputSource final : public InputSource
|
class SteamInputSource final : public InputSource {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
~SteamInputSource() override;
|
~SteamInputSource() override;
|
||||||
|
|
||||||
@@ -31,28 +29,16 @@ public:
|
|||||||
bool init(const std::string& manifest_absolute_path);
|
bool init(const std::string& manifest_absolute_path);
|
||||||
void shutdown();
|
void shutdown();
|
||||||
|
|
||||||
[[nodiscard]] const char* name() const override
|
[[nodiscard]] const char* name() const override { return name_; }
|
||||||
{
|
|
||||||
return name_;
|
|
||||||
}
|
|
||||||
void poll() override;
|
void poll() override;
|
||||||
[[nodiscard]] const std::array<PadInfo, kMaxPads>& pads() const override
|
[[nodiscard]] const std::array<PadInfo, kMaxPads>& pads() const override { return pads_; }
|
||||||
{
|
|
||||||
return pads_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward rumble to the guest: SteamInput TriggerVibration on the slot's
|
// Forward rumble to the guest: SteamInput TriggerVibration on the slot's
|
||||||
// controller when it's Steam-active, else the XInput fallback.
|
// controller when it's Steam-active, else the XInput fallback.
|
||||||
void set_rumble(int slot, std::uint16_t left, std::uint16_t right) override;
|
void set_rumble(int slot, std::uint16_t left, std::uint16_t right) override;
|
||||||
|
|
||||||
[[nodiscard]] bool steam_active() const
|
[[nodiscard]] bool steam_active() const { return steam_ready_; }
|
||||||
{
|
[[nodiscard]] int steam_controllers() const { return steam_count_; }
|
||||||
return steam_ready_;
|
|
||||||
}
|
|
||||||
[[nodiscard]] int steam_controllers() const
|
|
||||||
{
|
|
||||||
return steam_count_;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void resolve_handles();
|
void resolve_handles();
|
||||||
|
|||||||
@@ -3,19 +3,16 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <xinput.h>
|
#include <xinput.h>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
void XInputSource::poll()
|
void XInputSource::poll()
|
||||||
{
|
{
|
||||||
for (DWORD i = 0; i < kMaxPads; ++i)
|
for (DWORD i = 0; i < kMaxPads; ++i) {
|
||||||
{
|
|
||||||
XINPUT_STATE state = {};
|
XINPUT_STATE state = {};
|
||||||
const DWORD result = XInputGetState(i, &state);
|
const DWORD result = XInputGetState(i, &state);
|
||||||
|
|
||||||
PadInfo& info = pads_[i];
|
PadInfo& info = pads_[i];
|
||||||
if (result == ERROR_SUCCESS)
|
if (result == ERROR_SUCCESS) {
|
||||||
{
|
|
||||||
info.connected = true;
|
info.connected = true;
|
||||||
info.source = "XInput #" + std::to_string(i);
|
info.source = "XInput #" + std::to_string(i);
|
||||||
|
|
||||||
@@ -31,9 +28,7 @@ void XInputSource::poll()
|
|||||||
info.state.thumb_ly = g.sThumbLY;
|
info.state.thumb_ly = g.sThumbLY;
|
||||||
info.state.thumb_rx = g.sThumbRX;
|
info.state.thumb_rx = g.sThumbRX;
|
||||||
info.state.thumb_ry = g.sThumbRY;
|
info.state.thumb_ry = g.sThumbRY;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
info = PadInfo{};
|
info = PadInfo{};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,8 +36,7 @@ void XInputSource::poll()
|
|||||||
|
|
||||||
void XInputSource::set_rumble(int slot, std::uint16_t left, std::uint16_t right)
|
void XInputSource::set_rumble(int slot, std::uint16_t left, std::uint16_t right)
|
||||||
{
|
{
|
||||||
if (slot < 0 || slot >= static_cast<int>(kMaxPads))
|
if (slot < 0 || slot >= static_cast<int>(kMaxPads)) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
XINPUT_VIBRATION v{left, right};
|
XINPUT_VIBRATION v{left, right};
|
||||||
|
|||||||
@@ -2,25 +2,17 @@
|
|||||||
|
|
||||||
#include "input/input_source.hpp"
|
#include "input/input_source.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// Reads the four XInput slots. Remote Play Together exposes guest controllers
|
// Reads the four XInput slots. Remote Play Together exposes guest controllers
|
||||||
// here, alongside any controllers physically attached to the host.
|
// here, alongside any controllers physically attached to the host.
|
||||||
class XInputSource final : public InputSource
|
class XInputSource final : public InputSource {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
[[nodiscard]] const char* name() const override
|
[[nodiscard]] const char* name() const override { return "XInput"; }
|
||||||
{
|
|
||||||
return "XInput";
|
|
||||||
}
|
|
||||||
|
|
||||||
void poll() override;
|
void poll() override;
|
||||||
|
|
||||||
[[nodiscard]] const std::array<PadInfo, kMaxPads>& pads() const override
|
[[nodiscard]] const std::array<PadInfo, kMaxPads>& pads() const override { return pads_; }
|
||||||
{
|
|
||||||
return pads_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward rumble to the XInput device at `slot` (the guest's RPT virtual pad).
|
// Forward rumble to the XInput device at `slot` (the guest's RPT virtual pad).
|
||||||
void set_rumble(int slot, std::uint16_t left, std::uint16_t right) override;
|
void set_rumble(int slot, std::uint16_t left, std::uint16_t right) override;
|
||||||
|
|||||||
@@ -3,11 +3,9 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
// The hook writes these cumulative diagnostic counters cross-process (an x86 DLL can do a 64-bit
|
// The hook writes these cumulative diagnostic counters cross-process (an x86 DLL can do a 64-bit
|
||||||
// store in two halves), so read them atomically to avoid a torn value. The shared mapping is
|
// store in two halves), so read them atomically to avoid a torn value. The shared mapping is
|
||||||
// genuinely mutable -- the const here is just our read-only view -- so const_cast for atomic_ref.
|
// genuinely mutable -- the const here is just our read-only view -- so const_cast for atomic_ref.
|
||||||
@@ -22,8 +20,7 @@ bool IpcServer::start(unsigned long target_pid)
|
|||||||
std::scoped_lock lock(mutex_);
|
std::scoped_lock lock(mutex_);
|
||||||
stop_locked();
|
stop_locked();
|
||||||
|
|
||||||
if (!shm_.create(shared_memory_name(target_pid), sizeof(SharedBlock)))
|
if (!shm_.create(shared_memory_name(target_pid), sizeof(SharedBlock))) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,8 +37,7 @@ bool IpcServer::start(unsigned long target_pid)
|
|||||||
|
|
||||||
// Log ring: the injected hook opens this and streams its log lines back for the
|
// Log ring: the injected hook opens this and streams its log lines back for the
|
||||||
// Log window. Best-effort -- the rest of the tool works without it.
|
// Log window. Best-effort -- the rest of the tool works without it.
|
||||||
if (log_shm_.create(log_ring_name(target_pid), log_ring_total_size(kLogCapacity)))
|
if (log_shm_.create(log_ring_name(target_pid), log_ring_total_size(kLogCapacity))) {
|
||||||
{
|
|
||||||
log_ring_ = log_shm_.as<LogRing>();
|
log_ring_ = log_shm_.as<LogRing>();
|
||||||
log_ring_init(*log_ring_, kLogCapacity);
|
log_ring_init(*log_ring_, kLogCapacity);
|
||||||
log_cursor_ = 0;
|
log_cursor_ = 0;
|
||||||
@@ -52,13 +48,11 @@ bool IpcServer::start(unsigned long target_pid)
|
|||||||
void IpcServer::publish(const std::array<PadInfo, kMaxPads>& pads)
|
void IpcServer::publish(const std::array<PadInfo, kMaxPads>& pads)
|
||||||
{
|
{
|
||||||
std::scoped_lock lock(mutex_);
|
std::scoped_lock lock(mutex_);
|
||||||
if (block_ == nullptr)
|
if (block_ == nullptr) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
CoopPadState states[kMaxPads];
|
CoopPadState states[kMaxPads];
|
||||||
for (std::size_t i = 0; i < pads.size(); ++i)
|
for (std::size_t i = 0; i < pads.size(); ++i) {
|
||||||
{
|
|
||||||
states[i] = pads[i].state;
|
states[i] = pads[i].state;
|
||||||
states[i].connected = pads[i].connected ? 1 : 0;
|
states[i].connected = pads[i].connected ? 1 : 0;
|
||||||
}
|
}
|
||||||
@@ -69,21 +63,18 @@ HookStatusView IpcServer::hook_status() const
|
|||||||
{
|
{
|
||||||
std::scoped_lock lock(mutex_);
|
std::scoped_lock lock(mutex_);
|
||||||
HookStatusView view;
|
HookStatusView view;
|
||||||
if (block_ == nullptr)
|
if (block_ == nullptr) {
|
||||||
{
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
const HookStatus& s = block_->status;
|
const HookStatus& s = block_->status;
|
||||||
view.attached = s.attached != 0;
|
view.attached = s.attached != 0;
|
||||||
view.focus_spoof = s.focus_spoof != 0;
|
view.focus_spoof = s.focus_spoof != 0;
|
||||||
view.heartbeat = s.heartbeat.load(std::memory_order_relaxed);
|
view.heartbeat = s.heartbeat.load(std::memory_order_relaxed);
|
||||||
for (std::uint32_t i = 0; i < kMaxPads; ++i)
|
for (std::uint32_t i = 0; i < kMaxPads; ++i) {
|
||||||
{
|
|
||||||
view.get_state[i] = s.get_state_calls[i].load(std::memory_order_relaxed);
|
view.get_state[i] = s.get_state_calls[i].load(std::memory_order_relaxed);
|
||||||
view.get_caps[i] = s.get_caps_calls[i].load(std::memory_order_relaxed);
|
view.get_caps[i] = s.get_caps_calls[i].load(std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
for (std::uint32_t i = 0; i < FocusApi_Count; ++i)
|
for (std::uint32_t i = 0; i < FocusApi_Count; ++i) {
|
||||||
{
|
|
||||||
view.focus_calls[i] = s.focus_query_calls[i].load(std::memory_order_relaxed);
|
view.focus_calls[i] = s.focus_query_calls[i].load(std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
view.game_pid = s.game_pid;
|
view.game_pid = s.game_pid;
|
||||||
@@ -94,18 +85,15 @@ HookStatusView IpcServer::hook_status() const
|
|||||||
view.dinput_loaded = s.dinput_loaded != 0;
|
view.dinput_loaded = s.dinput_loaded != 0;
|
||||||
view.vk_too_late = s.vk_too_late != 0;
|
view.vk_too_late = s.vk_too_late != 0;
|
||||||
view.audio_streams_seen = s.audio_streams_seen;
|
view.audio_streams_seen = s.audio_streams_seen;
|
||||||
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i)
|
for (std::uint32_t i = 0; i < kMaxAudioStreams; ++i) {
|
||||||
{
|
|
||||||
view.audio_streams[i] = s.audio_streams[i];
|
view.audio_streams[i] = s.audio_streams[i];
|
||||||
view.audio_streams[i].frames_rendered = atomic_load_u64(s.audio_streams[i].frames_rendered);
|
view.audio_streams[i].frames_rendered = atomic_load_u64(s.audio_streams[i].frames_rendered);
|
||||||
}
|
}
|
||||||
view.hook_entry_count = s.hook_entry_count;
|
view.hook_entry_count = s.hook_entry_count;
|
||||||
for (std::uint32_t i = 0; i < kMaxHookEntries; ++i)
|
for (std::uint32_t i = 0; i < kMaxHookEntries; ++i) {
|
||||||
{
|
|
||||||
view.hook_entries[i] = s.hook_entries[i];
|
view.hook_entries[i] = s.hook_entries[i];
|
||||||
}
|
}
|
||||||
for (std::uint32_t i = 0; i < kMaxPads; ++i)
|
for (std::uint32_t i = 0; i < kMaxPads; ++i) {
|
||||||
{
|
|
||||||
view.rumble_left[i] = s.rumble_left[i];
|
view.rumble_left[i] = s.rumble_left[i];
|
||||||
view.rumble_right[i] = s.rumble_right[i];
|
view.rumble_right[i] = s.rumble_right[i];
|
||||||
view.read_state[i] = s.read_state[i];
|
view.read_state[i] = s.read_state[i];
|
||||||
@@ -117,8 +105,7 @@ VideoShareView IpcServer::video_share() const
|
|||||||
{
|
{
|
||||||
std::scoped_lock lock(mutex_);
|
std::scoped_lock lock(mutex_);
|
||||||
VideoShareView v;
|
VideoShareView v;
|
||||||
if (block_ == nullptr)
|
if (block_ == nullptr) {
|
||||||
{
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
const VideoShare& s = block_->video;
|
const VideoShare& s = block_->video;
|
||||||
@@ -135,8 +122,7 @@ VideoShareView IpcServer::video_share() const
|
|||||||
void IpcServer::set_subsystem_enabled(std::uint32_t subsystem, bool enabled)
|
void IpcServer::set_subsystem_enabled(std::uint32_t subsystem, bool enabled)
|
||||||
{
|
{
|
||||||
std::scoped_lock lock(mutex_);
|
std::scoped_lock lock(mutex_);
|
||||||
if (block_ != nullptr && subsystem < HookSubsys_Count)
|
if (block_ != nullptr && subsystem < HookSubsys_Count) {
|
||||||
{
|
|
||||||
// 0 = install, 1 = remove.
|
// 0 = install, 1 = remove.
|
||||||
block_->control.subsystem_disabled[subsystem].store(enabled ? 0u : 1u, std::memory_order_release);
|
block_->control.subsystem_disabled[subsystem].store(enabled ? 0u : 1u, std::memory_order_release);
|
||||||
}
|
}
|
||||||
@@ -145,12 +131,10 @@ void IpcServer::set_subsystem_enabled(std::uint32_t subsystem, bool enabled)
|
|||||||
void IpcServer::request_unhook_all()
|
void IpcServer::request_unhook_all()
|
||||||
{
|
{
|
||||||
std::scoped_lock lock(mutex_);
|
std::scoped_lock lock(mutex_);
|
||||||
if (block_ == nullptr)
|
if (block_ == nullptr) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (std::uint32_t s = 0; s < HookSubsys_Count; ++s)
|
for (std::uint32_t s = 0; s < HookSubsys_Count; ++s) {
|
||||||
{
|
|
||||||
block_->control.subsystem_disabled[s].store(1u, std::memory_order_release); // 1 = remove
|
block_->control.subsystem_disabled[s].store(1u, std::memory_order_release); // 1 = remove
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,20 +142,16 @@ void IpcServer::request_unhook_all()
|
|||||||
bool IpcServer::all_hooks_removed() const
|
bool IpcServer::all_hooks_removed() const
|
||||||
{
|
{
|
||||||
std::scoped_lock lock(mutex_);
|
std::scoped_lock lock(mutex_);
|
||||||
if (block_ == nullptr)
|
if (block_ == nullptr) {
|
||||||
{
|
|
||||||
return true; // not connected -> nothing of ours is hooked
|
return true; // not connected -> nothing of ours is hooked
|
||||||
}
|
}
|
||||||
const HookStatus& s = block_->status;
|
const HookStatus& s = block_->status;
|
||||||
std::uint32_t count = s.hook_entry_count;
|
std::uint32_t count = s.hook_entry_count;
|
||||||
if (count > kMaxHookEntries)
|
if (count > kMaxHookEntries) {
|
||||||
{
|
|
||||||
count = kMaxHookEntries;
|
count = kMaxHookEntries;
|
||||||
}
|
}
|
||||||
for (std::uint32_t i = 0; i < count; ++i)
|
for (std::uint32_t i = 0; i < count; ++i) {
|
||||||
{
|
if (s.hook_entries[i].installed != 0) {
|
||||||
if (s.hook_entries[i].installed != 0)
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,8 +161,7 @@ bool IpcServer::all_hooks_removed() const
|
|||||||
void IpcServer::host_log(std::uint32_t level, const char* text)
|
void IpcServer::host_log(std::uint32_t level, const char* text)
|
||||||
{
|
{
|
||||||
std::scoped_lock lock(mutex_);
|
std::scoped_lock lock(mutex_);
|
||||||
if (log_ring_ != nullptr)
|
if (log_ring_ != nullptr) {
|
||||||
{
|
|
||||||
log_ring_push(*log_ring_, GetCurrentProcessId(), level, GetTickCount64(), text);
|
log_ring_push(*log_ring_, GetCurrentProcessId(), level, GetTickCount64(), text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,8 +174,7 @@ void IpcServer::stop()
|
|||||||
|
|
||||||
void IpcServer::stop_locked()
|
void IpcServer::stop_locked()
|
||||||
{
|
{
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr) {
|
||||||
{
|
|
||||||
block_->magic = 0; // invalidate so a late hook read won't trust stale data
|
block_->magic = 0; // invalidate so a late hook read won't trust stale data
|
||||||
block_ = nullptr;
|
block_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,10 @@
|
|||||||
#include "coop/shared_memory.hpp"
|
#include "coop/shared_memory.hpp"
|
||||||
#include "input/input_source.hpp"
|
#include "input/input_source.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// Plain (non-atomic) snapshot of the hook's back-channel for the overlay.
|
// Plain (non-atomic) snapshot of the hook's back-channel for the overlay.
|
||||||
struct HookStatusView
|
struct HookStatusView {
|
||||||
{
|
|
||||||
bool attached = false; // XInput hooks installed in the game
|
bool attached = false; // XInput hooks installed in the game
|
||||||
bool focus_spoof = false; // focus spoofing active
|
bool focus_spoof = false; // focus spoofing active
|
||||||
std::uint32_t heartbeat = 0; // DLL liveness counter
|
std::uint32_t heartbeat = 0; // DLL liveness counter
|
||||||
@@ -48,8 +46,7 @@ struct HookStatusView
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Plain snapshot of the Present-hook video channel for the Video mirror panel.
|
// Plain snapshot of the Present-hook video channel for the Video mirror panel.
|
||||||
struct VideoShareView
|
struct VideoShareView {
|
||||||
{
|
|
||||||
std::uint32_t generation = 0; // bumps per shared frame; 0 = nothing shared yet
|
std::uint32_t generation = 0; // bumps per shared frame; 0 = nothing shared yet
|
||||||
std::uint32_t width = 0; // shared texture dimensions / DXGI format
|
std::uint32_t width = 0; // shared texture dimensions / DXGI format
|
||||||
std::uint32_t height = 0;
|
std::uint32_t height = 0;
|
||||||
@@ -59,8 +56,7 @@ struct VideoShareView
|
|||||||
std::uint64_t frames_dropped = 0; // cumulative captures skipped (mutex busy at present)
|
std::uint64_t frames_dropped = 0; // cumulative captures skipped (mutex busy at present)
|
||||||
};
|
};
|
||||||
|
|
||||||
class IpcServer
|
class IpcServer {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
// Creates and initializes the section for `target_pid`. The injected hook
|
// Creates and initializes the section for `target_pid`. The injected hook
|
||||||
// derives the same name from its own pid and opens it.
|
// derives the same name from its own pid and opens it.
|
||||||
@@ -98,8 +94,7 @@ public:
|
|||||||
void push_mkb(const MkbEvent& ev)
|
void push_mkb(const MkbEvent& ev)
|
||||||
{
|
{
|
||||||
std::scoped_lock lock(mutex_);
|
std::scoped_lock lock(mutex_);
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr) {
|
||||||
{
|
|
||||||
push_mkb_event(block_->mkb, ev);
|
push_mkb_event(block_->mkb, ev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,8 +104,7 @@ public:
|
|||||||
void set_cursor_clip_allowed(bool allowed)
|
void set_cursor_clip_allowed(bool allowed)
|
||||||
{
|
{
|
||||||
std::scoped_lock lock(mutex_);
|
std::scoped_lock lock(mutex_);
|
||||||
if (block_ != nullptr)
|
if (block_ != nullptr) {
|
||||||
{
|
|
||||||
block_->control.allow_cursor_clip.store(allowed ? 1u : 0u, std::memory_order_release);
|
block_->control.allow_cursor_clip.store(allowed ? 1u : 0u, std::memory_order_release);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,8 +114,7 @@ public:
|
|||||||
template <typename F>
|
template <typename F>
|
||||||
void drain_logs(F&& emit)
|
void drain_logs(F&& emit)
|
||||||
{
|
{
|
||||||
if (log_ring_ != nullptr)
|
if (log_ring_ != nullptr) {
|
||||||
{
|
|
||||||
log_ring_drain(*log_ring_, log_cursor_, emit);
|
log_ring_drain(*log_ring_, log_cursor_, emit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,14 +123,8 @@ public:
|
|||||||
// shows color-coded in the Log window next to the hook's lines. No-op if not started.
|
// shows color-coded in the Log window next to the hook's lines. No-op if not started.
|
||||||
void host_log(std::uint32_t level, const char* text);
|
void host_log(std::uint32_t level, const char* text);
|
||||||
|
|
||||||
[[nodiscard]] bool running() const
|
[[nodiscard]] bool running() const { return block_ != nullptr; }
|
||||||
{
|
[[nodiscard]] unsigned long target_pid() const { return target_pid_; }
|
||||||
return block_ != nullptr;
|
|
||||||
}
|
|
||||||
[[nodiscard]] unsigned long target_pid() const
|
|
||||||
{
|
|
||||||
return target_pid_;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void stop_locked(); // tear-down body shared by start()/stop(); caller holds mutex_
|
void stop_locked(); // tear-down body shared by start()/stop(); caller holds mutex_
|
||||||
|
|||||||
@@ -8,13 +8,11 @@
|
|||||||
#include "ui/app_chrome.hpp"
|
#include "ui/app_chrome.hpp"
|
||||||
#include "ui/text_match.hpp"
|
#include "ui/text_match.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
void LogPanel::add_line(const LogRecord& rec)
|
void LogPanel::add_line(const LogRecord& rec)
|
||||||
{
|
{
|
||||||
if (first_millis_ == 0)
|
if (first_millis_ == 0) {
|
||||||
{
|
|
||||||
first_millis_ = rec.millis;
|
first_millis_ = rec.millis;
|
||||||
}
|
}
|
||||||
const double secs = static_cast<double>(rec.millis - first_millis_) / 1000.0;
|
const double secs = static_cast<double>(rec.millis - first_millis_) / 1000.0;
|
||||||
@@ -22,8 +20,7 @@ void LogPanel::add_line(const LogRecord& rec)
|
|||||||
char buf[256];
|
char buf[256];
|
||||||
std::snprintf(buf, sizeof(buf), "[%8.3f] %s", secs, rec.text);
|
std::snprintf(buf, sizeof(buf), "[%8.3f] %s", secs, rec.text);
|
||||||
lines_.push_back({buf, rec.level});
|
lines_.push_back({buf, rec.level});
|
||||||
while (lines_.size() > kMaxLines)
|
while (lines_.size() > kMaxLines) {
|
||||||
{
|
|
||||||
lines_.pop_front();
|
lines_.pop_front();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,8 +35,7 @@ void LogPanel::draw()
|
|||||||
apply_panel_layout(Panel::Log);
|
apply_panel_layout(Panel::Log);
|
||||||
ImGui::Begin("Log");
|
ImGui::Begin("Log");
|
||||||
|
|
||||||
if (ImGui::Button("Clear"))
|
if (ImGui::Button("Clear")) {
|
||||||
{
|
|
||||||
lines_.clear();
|
lines_.clear();
|
||||||
first_millis_ = 0;
|
first_millis_ = 0;
|
||||||
}
|
}
|
||||||
@@ -50,17 +46,13 @@ void LogPanel::draw()
|
|||||||
ImGui::InputTextWithHint("##logfilter", "filter...", filter_, sizeof(filter_));
|
ImGui::InputTextWithHint("##logfilter", "filter...", filter_, sizeof(filter_));
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
if (ImGui::BeginChild("loglines", ImVec2(0, 0), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar))
|
if (ImGui::BeginChild("loglines", ImVec2(0, 0), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar)) {
|
||||||
{
|
|
||||||
const bool has_filter = filter_[0] != '\0';
|
const bool has_filter = filter_[0] != '\0';
|
||||||
for (const Line& line : lines_)
|
for (const Line& line : lines_) {
|
||||||
{
|
if (has_filter && !contains_ci(line.text, filter_)) {
|
||||||
if (has_filter && !contains_ci(line.text, filter_))
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
switch (line.level)
|
switch (line.level) {
|
||||||
{
|
|
||||||
case LogLevel_Warn:
|
case LogLevel_Warn:
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", line.text.c_str()); // amber
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", line.text.c_str()); // amber
|
||||||
break;
|
break;
|
||||||
@@ -73,8 +65,7 @@ void LogPanel::draw()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Stick to the bottom while new lines arrive (unless the user scrolled up).
|
// Stick to the bottom while new lines arrive (unless the user scrolled up).
|
||||||
if (autoscroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 1.0f)
|
if (autoscroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 1.0f) {
|
||||||
{
|
|
||||||
ImGui::SetScrollHereY(1.0f);
|
ImGui::SetScrollHereY(1.0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,11 @@
|
|||||||
|
|
||||||
#include "coop/log_ring.hpp"
|
#include "coop/log_ring.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
class InjectionPanel;
|
class InjectionPanel;
|
||||||
|
|
||||||
class LogPanel
|
class LogPanel {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
// Pull any new lines the hook emitted (call once per frame before draw()).
|
// Pull any new lines the hook emitted (call once per frame before draw()).
|
||||||
void pull(InjectionPanel& injection);
|
void pull(InjectionPanel& injection);
|
||||||
@@ -25,8 +23,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
void add_line(const LogRecord& rec);
|
void add_line(const LogRecord& rec);
|
||||||
|
|
||||||
struct Line
|
struct Line {
|
||||||
{
|
|
||||||
std::string text;
|
std::string text;
|
||||||
std::uint32_t level; // LogLevel, for colouring
|
std::uint32_t level; // LogLevel, for colouring
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,8 +38,7 @@
|
|||||||
#include "util/utf8.hpp"
|
#include "util/utf8.hpp"
|
||||||
#include "vk_layer_setup.hpp"
|
#include "vk_layer_setup.hpp"
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
|
|
||||||
// Timestamped screenshot path next to the exe (e.g. coop_shot_20260622_143501.png).
|
// Timestamped screenshot path next to the exe (e.g. coop_shot_20260622_143501.png).
|
||||||
std::wstring screenshot_path()
|
std::wstring screenshot_path()
|
||||||
@@ -47,8 +46,8 @@ std::wstring screenshot_path()
|
|||||||
SYSTEMTIME st{};
|
SYSTEMTIME st{};
|
||||||
GetLocalTime(&st);
|
GetLocalTime(&st);
|
||||||
wchar_t name[64];
|
wchar_t name[64];
|
||||||
swprintf(name, static_cast<int>(std::size(name)), L"coop_shot_%04u%02u%02u_%02u%02u%02u.png", st.wYear,
|
swprintf(name, static_cast<int>(std::size(name)), L"coop_shot_%04u%02u%02u_%02u%02u%02u.png", st.wYear, st.wMonth,
|
||||||
st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
|
st.wDay, st.wHour, st.wMinute, st.wSecond);
|
||||||
return coop::exe_directory() + name;
|
return coop::exe_directory() + name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,12 +56,11 @@ std::string screenshot_basename(const std::wstring& path)
|
|||||||
{
|
{
|
||||||
const std::size_t slash = path.find_last_of(L"\\/");
|
const std::size_t slash = path.find_last_of(L"\\/");
|
||||||
const std::wstring file = slash == std::wstring::npos ? path : path.substr(slash + 1);
|
const std::wstring file = slash == std::wstring::npos ? path : path.substr(slash + 1);
|
||||||
if (file.empty())
|
if (file.empty()) {
|
||||||
{
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const int n = WideCharToMultiByte(CP_UTF8, 0, file.c_str(), static_cast<int>(file.size()), nullptr, 0,
|
const int n =
|
||||||
nullptr, nullptr);
|
WideCharToMultiByte(CP_UTF8, 0, file.c_str(), static_cast<int>(file.size()), nullptr, 0, nullptr, nullptr);
|
||||||
std::string out(static_cast<std::size_t>(n), '\0');
|
std::string out(static_cast<std::size_t>(n), '\0');
|
||||||
WideCharToMultiByte(CP_UTF8, 0, file.c_str(), static_cast<int>(file.size()), out.data(), n, nullptr, nullptr);
|
WideCharToMultiByte(CP_UTF8, 0, file.c_str(), static_cast<int>(file.size()), out.data(), n, nullptr, nullptr);
|
||||||
return out;
|
return out;
|
||||||
@@ -74,16 +72,15 @@ std::string screenshot_basename(const std::wstring& path)
|
|||||||
void draw_screenshot_toast(double seconds_since, const std::string& name)
|
void draw_screenshot_toast(double seconds_since, const std::string& name)
|
||||||
{
|
{
|
||||||
const float fade = 1.0f - static_cast<float>(seconds_since) / 2.5f;
|
const float fade = 1.0f - static_cast<float>(seconds_since) / 2.5f;
|
||||||
if (fade <= 0.0f || name.empty())
|
if (fade <= 0.0f || name.empty()) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ImGuiViewport* vp = ImGui::GetMainViewport();
|
const ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||||
ImGui::SetNextWindowPos(ImVec2(vp->WorkPos.x + 12.0f, vp->WorkPos.y + vp->WorkSize.y - 44.0f));
|
ImGui::SetNextWindowPos(ImVec2(vp->WorkPos.x + 12.0f, vp->WorkPos.y + vp->WorkSize.y - 44.0f));
|
||||||
ImGui::SetNextWindowBgAlpha(0.45f * fade);
|
ImGui::SetNextWindowBgAlpha(0.45f * fade);
|
||||||
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs |
|
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs
|
||||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings |
|
| ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings
|
||||||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav;
|
| ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav;
|
||||||
ImGui::Begin("##shot_toast", nullptr, flags);
|
ImGui::Begin("##shot_toast", nullptr, flags);
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 1.0f, 0.6f, fade));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 1.0f, 0.6f, fade));
|
||||||
ImGui::Text("Saved screenshot: %s", name.c_str());
|
ImGui::Text("Saved screenshot: %s", name.c_str());
|
||||||
@@ -103,98 +100,84 @@ std::string apply_test_command(const std::string& cmd, coop::UiState& ui, coop::
|
|||||||
{
|
{
|
||||||
std::istringstream is(cmd);
|
std::istringstream is(cmd);
|
||||||
std::string t;
|
std::string t;
|
||||||
while (is >> t)
|
while (is >> t) {
|
||||||
{
|
|
||||||
tok.push_back(t);
|
tok.push_back(t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tok.empty())
|
if (tok.empty()) {
|
||||||
{
|
|
||||||
return "empty";
|
return "empty";
|
||||||
}
|
}
|
||||||
const std::string& v = tok[0];
|
const std::string& v = tok[0];
|
||||||
auto arg = [&](std::size_t i) -> std::string { return i < tok.size() ? tok[i] : std::string(); };
|
auto arg = [&](std::size_t i) -> std::string { return i < tok.size() ? tok[i] : std::string(); };
|
||||||
auto num = [&](std::size_t i) -> unsigned { return static_cast<unsigned>(std::strtoul(arg(i).c_str(), nullptr, 10)); };
|
auto num = [&](std::size_t i) -> unsigned {
|
||||||
|
return static_cast<unsigned>(std::strtoul(arg(i).c_str(), nullptr, 10));
|
||||||
|
};
|
||||||
|
|
||||||
if (v == "inject")
|
if (v == "inject") {
|
||||||
{
|
|
||||||
const unsigned long pid = injection.dev_inject_by_name(widen(arg(1)));
|
const unsigned long pid = injection.dev_inject_by_name(widen(arg(1)));
|
||||||
return pid != 0 ? ("ok pid " + std::to_string(pid)) : "fail no-process-or-inject-failed";
|
return pid != 0 ? ("ok pid " + std::to_string(pid)) : "fail no-process-or-inject-failed";
|
||||||
}
|
}
|
||||||
if (v == "audio")
|
if (v == "audio") {
|
||||||
{
|
|
||||||
const bool on = arg(1) == "on";
|
const bool on = arg(1) == "on";
|
||||||
if (on)
|
if (on) {
|
||||||
{
|
|
||||||
audio.dev_set_pid(injection.target_pid());
|
audio.dev_set_pid(injection.target_pid());
|
||||||
}
|
}
|
||||||
audio.dev_set_enabled(on);
|
audio.dev_set_enabled(on);
|
||||||
return "ok";
|
return "ok";
|
||||||
}
|
}
|
||||||
if (v == "video")
|
if (v == "video") {
|
||||||
{
|
|
||||||
// Install/remove the hooked video subsystem (Present/GL/D3D9/Vulkan capture hooks).
|
// Install/remove the hooked video subsystem (Present/GL/D3D9/Vulkan capture hooks).
|
||||||
injection.request_video(arg(1) == "on");
|
injection.request_video(arg(1) == "on");
|
||||||
return "ok";
|
return "ok";
|
||||||
}
|
}
|
||||||
if (v == "debug")
|
if (v == "debug") {
|
||||||
{
|
|
||||||
ui.debug_details = (arg(1) == "on");
|
ui.debug_details = (arg(1) == "on");
|
||||||
return "ok";
|
return "ok";
|
||||||
}
|
}
|
||||||
if (v == "autoattach")
|
if (v == "autoattach") {
|
||||||
{
|
|
||||||
injection.dev_set_auto_reattach(arg(1) == "on");
|
injection.dev_set_auto_reattach(arg(1) == "on");
|
||||||
return "ok";
|
return "ok";
|
||||||
}
|
}
|
||||||
if (v == "remeasure")
|
if (v == "remeasure") {
|
||||||
{
|
|
||||||
audio.dev_request_op(num(1), coop::AudioRingOp_Remeasure, 0, 0, 0, 0);
|
audio.dev_request_op(num(1), coop::AudioRingOp_Remeasure, 0, 0, 0, 0);
|
||||||
return "ok";
|
return "ok";
|
||||||
}
|
}
|
||||||
if (v == "override")
|
if (v == "override") {
|
||||||
{
|
|
||||||
const std::uint32_t tag = arg(5) == "float" ? static_cast<std::uint32_t>(WAVE_FORMAT_IEEE_FLOAT)
|
const std::uint32_t tag = arg(5) == "float" ? static_cast<std::uint32_t>(WAVE_FORMAT_IEEE_FLOAT)
|
||||||
: static_cast<std::uint32_t>(WAVE_FORMAT_PCM);
|
: static_cast<std::uint32_t>(WAVE_FORMAT_PCM);
|
||||||
audio.dev_request_op(num(1), coop::AudioRingOp_Override, num(2), num(3), num(4), tag);
|
audio.dev_request_op(num(1), coop::AudioRingOp_Override, num(2), num(3), num(4), tag);
|
||||||
return "ok";
|
return "ok";
|
||||||
}
|
}
|
||||||
if (v == "screenshot")
|
if (v == "screenshot") {
|
||||||
{
|
|
||||||
const std::wstring p = screenshot_path();
|
const std::wstring p = screenshot_path();
|
||||||
window.request_screenshot(p);
|
window.request_screenshot(p);
|
||||||
return "ok";
|
return "ok";
|
||||||
}
|
}
|
||||||
if (v == "uisize")
|
if (v == "uisize") {
|
||||||
{
|
|
||||||
// Force a reference layout size so the UI-fit check is monitor-independent.
|
// Force a reference layout size so the UI-fit check is monitor-independent.
|
||||||
coop::set_layout_reference(static_cast<float>(num(1)), static_cast<float>(num(2)));
|
coop::set_layout_reference(static_cast<float>(num(1)), static_cast<float>(num(2)));
|
||||||
return "ok";
|
return "ok";
|
||||||
}
|
}
|
||||||
if (v == "uifit")
|
if (v == "uifit") {
|
||||||
{
|
|
||||||
// Report any panel whose content overflowed its assigned size last frame.
|
// Report any panel whose content overflowed its assigned size last frame.
|
||||||
char buf[256];
|
char buf[256];
|
||||||
coop::panel_fit_report(buf, sizeof(buf));
|
coop::panel_fit_report(buf, sizeof(buf));
|
||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
if (v == "quit")
|
if (v == "quit") {
|
||||||
{
|
|
||||||
ui.request_quit = true;
|
ui.request_quit = true;
|
||||||
return "ok";
|
return "ok";
|
||||||
}
|
}
|
||||||
if (v == "status")
|
if (v == "status") {
|
||||||
{
|
|
||||||
const coop::HookStatusView st = injection.hook_status();
|
const coop::HookStatusView st = injection.hook_status();
|
||||||
const std::string reason = audio.dev_reason();
|
const std::string reason = audio.dev_reason();
|
||||||
char buf[512];
|
char buf[512];
|
||||||
std::snprintf(buf, sizeof(buf),
|
std::snprintf(buf, sizeof(buf),
|
||||||
"audio_running=%d source=%s rate=%u ch=%u state=%u streams=%u inj_pid=%lu inj_state=%d "
|
"audio_running=%d source=%s rate=%u ch=%u state=%u streams=%u inj_pid=%lu inj_state=%d "
|
||||||
"reason=%s",
|
"reason=%s",
|
||||||
audio.dev_running() ? 1 : 0, audio.dev_source().c_str(), audio.dev_rate(),
|
audio.dev_running() ? 1 : 0, audio.dev_source().c_str(), audio.dev_rate(), audio.dev_channels(),
|
||||||
audio.dev_channels(), st.audio_streams[0].format_state, st.audio_streams_seen,
|
st.audio_streams[0].format_state, st.audio_streams_seen, injection.target_pid(),
|
||||||
injection.target_pid(), static_cast<int>(injection.target_state()),
|
static_cast<int>(injection.target_state()), reason.empty() ? "-" : reason.c_str());
|
||||||
reason.empty() ? "-" : reason.c_str());
|
|
||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
return "unknown-command";
|
return "unknown-command";
|
||||||
@@ -209,8 +192,7 @@ std::string steam_manifest_path()
|
|||||||
const DWORD len = GetModuleFileNameA(nullptr, buffer, MAX_PATH);
|
const DWORD len = GetModuleFileNameA(nullptr, buffer, MAX_PATH);
|
||||||
std::string path(buffer, len);
|
std::string path(buffer, len);
|
||||||
const std::size_t slash = path.find_last_of("\\/");
|
const std::size_t slash = path.find_last_of("\\/");
|
||||||
if (slash != std::string::npos)
|
if (slash != std::string::npos) {
|
||||||
{
|
|
||||||
path.resize(slash + 1);
|
path.resize(slash + 1);
|
||||||
}
|
}
|
||||||
return path + "steam_input_actions.vdf";
|
return path + "steam_input_actions.vdf";
|
||||||
@@ -226,16 +208,15 @@ void draw_vk_too_late_banner()
|
|||||||
{
|
{
|
||||||
const ImGuiViewport* vp = ImGui::GetMainViewport();
|
const ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||||
float w = vp->WorkSize.x - 40.0f;
|
float w = vp->WorkSize.x - 40.0f;
|
||||||
if (w > 760.0f)
|
if (w > 760.0f) {
|
||||||
{
|
|
||||||
w = 760.0f;
|
w = 760.0f;
|
||||||
}
|
}
|
||||||
ImGui::SetNextWindowPos(ImVec2(vp->WorkPos.x + vp->WorkSize.x * 0.5f, vp->WorkPos.y + 16.0f),
|
ImGui::SetNextWindowPos(ImVec2(vp->WorkPos.x + vp->WorkSize.x * 0.5f, vp->WorkPos.y + 16.0f), ImGuiCond_Always,
|
||||||
ImGuiCond_Always, ImVec2(0.5f, 0.0f));
|
ImVec2(0.5f, 0.0f));
|
||||||
ImGui::SetNextWindowSize(ImVec2(w, 0.0f));
|
ImGui::SetNextWindowSize(ImVec2(w, 0.0f));
|
||||||
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs |
|
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs
|
||||||
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
|
| ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing
|
||||||
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_AlwaysAutoResize;
|
| ImGuiWindowFlags_NoNav | ImGuiWindowFlags_AlwaysAutoResize;
|
||||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.28f, 0.03f, 0.03f, 0.92f));
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.28f, 0.03f, 0.03f, 0.92f));
|
||||||
ImGui::Begin("##vk_too_late", nullptr, flags);
|
ImGui::Begin("##vk_too_late", nullptr, flags);
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.5f, 0.45f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.5f, 0.45f, 1.0f));
|
||||||
@@ -255,15 +236,14 @@ void draw_vk_too_late_banner()
|
|||||||
void draw_overlay_hidden_hint(double seconds_hidden)
|
void draw_overlay_hidden_hint(double seconds_hidden)
|
||||||
{
|
{
|
||||||
const float fade = 1.0f - static_cast<float>(seconds_hidden) / 4.0f;
|
const float fade = 1.0f - static_cast<float>(seconds_hidden) / 4.0f;
|
||||||
if (fade <= 0.0f)
|
if (fade <= 0.0f) {
|
||||||
{
|
|
||||||
return; // fully faded -> truly clean window for RPT capture
|
return; // fully faded -> truly clean window for RPT capture
|
||||||
}
|
}
|
||||||
ImGui::SetNextWindowPos(ImVec2(12.0f, 12.0f));
|
ImGui::SetNextWindowPos(ImVec2(12.0f, 12.0f));
|
||||||
ImGui::SetNextWindowBgAlpha(0.35f * fade);
|
ImGui::SetNextWindowBgAlpha(0.35f * fade);
|
||||||
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs |
|
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs
|
||||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings |
|
| ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings
|
||||||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav;
|
| ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav;
|
||||||
ImGui::Begin("##overlay_hint", nullptr, flags);
|
ImGui::Begin("##overlay_hint", nullptr, flags);
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, fade));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, fade));
|
||||||
ImGui::TextUnformatted("F1: show overlay");
|
ImGui::TextUnformatted("F1: show overlay");
|
||||||
@@ -281,11 +261,9 @@ bool wait_for_hooked_frame(coop::D3D11Window& window, coop::InjectionPanel& inje
|
|||||||
QueryPerformanceFrequency(&freq);
|
QueryPerformanceFrequency(&freq);
|
||||||
QueryPerformanceCounter(&start);
|
QueryPerformanceCounter(&start);
|
||||||
constexpr double kTimeoutMs = 200.0; // present anyway if the game stalls / is paused
|
constexpr double kTimeoutMs = 200.0; // present anyway if the game stalls / is paused
|
||||||
for (;;)
|
for (;;) {
|
||||||
{
|
|
||||||
const std::uint32_t gen = injection.video_share().generation;
|
const std::uint32_t gen = injection.video_share().generation;
|
||||||
if (gen != last_gen)
|
if (gen != last_gen) {
|
||||||
{
|
|
||||||
last_gen = gen;
|
last_gen = gen;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -293,13 +271,11 @@ bool wait_for_hooked_frame(coop::D3D11Window& window, coop::InjectionPanel& inje
|
|||||||
QueryPerformanceCounter(&now);
|
QueryPerformanceCounter(&now);
|
||||||
const double elapsed =
|
const double elapsed =
|
||||||
static_cast<double>(now.QuadPart - start.QuadPart) * 1000.0 / static_cast<double>(freq.QuadPart);
|
static_cast<double>(now.QuadPart - start.QuadPart) * 1000.0 / static_cast<double>(freq.QuadPart);
|
||||||
if (elapsed >= kTimeoutMs)
|
if (elapsed >= kTimeoutMs) {
|
||||||
{
|
|
||||||
last_gen = gen;
|
last_gen = gen;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!window.pump_messages())
|
if (!window.pump_messages()) {
|
||||||
{
|
|
||||||
return false; // WM_QUIT
|
return false; // WM_QUIT
|
||||||
}
|
}
|
||||||
Sleep(1); // yield ~1 ms (timeBeginPeriod(1) keeps this granular) instead of busy-spinning
|
Sleep(1); // yield ~1 ms (timeBeginPeriod(1) keeps this granular) instead of busy-spinning
|
||||||
@@ -313,8 +289,7 @@ int run()
|
|||||||
coop::cleanup_stale_vk_layer();
|
coop::cleanup_stale_vk_layer();
|
||||||
|
|
||||||
coop::D3D11Window window;
|
coop::D3D11Window window;
|
||||||
if (!window.create(L"CoopAllTheThings"))
|
if (!window.create(L"CoopAllTheThings")) {
|
||||||
{
|
|
||||||
MessageBoxW(nullptr, L"Failed to create the D3D11 window.", L"CoopAllTheThings", MB_ICONERROR);
|
MessageBoxW(nullptr, L"Failed to create the D3D11 window.", L"CoopAllTheThings", MB_ICONERROR);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -324,8 +299,7 @@ int run()
|
|||||||
// Declaring ui first means it's destroyed AFTER imgui, so that final save never reads freed state.
|
// Declaring ui first means it's destroyed AFTER imgui, so that final save never reads freed state.
|
||||||
coop::UiState ui;
|
coop::UiState ui;
|
||||||
coop::ImGuiLayer imgui;
|
coop::ImGuiLayer imgui;
|
||||||
if (!imgui.init(window.hwnd(), window.device(), window.context()))
|
if (!imgui.init(window.hwnd(), window.device(), window.context())) {
|
||||||
{
|
|
||||||
MessageBoxW(nullptr, L"Failed to initialize ImGui.", L"CoopAllTheThings", MB_ICONERROR);
|
MessageBoxW(nullptr, L"Failed to initialize ImGui.", L"CoopAllTheThings", MB_ICONERROR);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -335,8 +309,7 @@ int run()
|
|||||||
coop::AudioPanel audio;
|
coop::AudioPanel audio;
|
||||||
coop::CapturePanel capture;
|
coop::CapturePanel capture;
|
||||||
coop::LogPanel log;
|
coop::LogPanel log;
|
||||||
if (!capture.init(window.device()))
|
if (!capture.init(window.device())) {
|
||||||
{
|
|
||||||
MessageBoxW(nullptr, L"Failed to initialize the video mirror.", L"CoopAllTheThings", MB_ICONERROR);
|
MessageBoxW(nullptr, L"Failed to initialize the video mirror.", L"CoopAllTheThings", MB_ICONERROR);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -376,22 +349,18 @@ int run()
|
|||||||
// Frame-sync: the hook generation we last presented (so we wait for the next one).
|
// Frame-sync: the hook generation we last presented (so we wait for the next one).
|
||||||
std::uint32_t last_synced_gen = 0;
|
std::uint32_t last_synced_gen = 0;
|
||||||
|
|
||||||
while (window.pump_messages())
|
while (window.pump_messages()) {
|
||||||
{
|
|
||||||
// Rescale the overlay if the window changed DPI (moved monitors, or the display scale changed).
|
// Rescale the overlay if the window changed DPI (moved monitors, or the display scale changed).
|
||||||
// pump_messages() latches the new DPI; apply it here, outside any in-progress ImGui frame.
|
// pump_messages() latches the new DPI; apply it here, outside any in-progress ImGui frame.
|
||||||
if (unsigned new_dpi = 0; window.take_dpi_change(new_dpi))
|
if (unsigned new_dpi = 0; window.take_dpi_change(new_dpi)) {
|
||||||
{
|
|
||||||
imgui.set_dpi(new_dpi);
|
imgui.set_dpi(new_dpi);
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the operator enabled "Sync flip to game frames" (Hooked source), pace the
|
// When the operator enabled "Sync flip to game frames" (Hooked source), pace the
|
||||||
// whole iteration to the game: wait for the next published frame before rendering,
|
// whole iteration to the game: wait for the next published frame before rendering,
|
||||||
// then present without vsync so the flip lands in lockstep with the game.
|
// then present without vsync so the flip lands in lockstep with the game.
|
||||||
if (capture.frame_sync_active())
|
if (capture.frame_sync_active()) {
|
||||||
{
|
if (!wait_for_hooked_frame(window, injection, last_synced_gen)) {
|
||||||
if (!wait_for_hooked_frame(window, injection, last_synced_gen))
|
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -401,12 +370,9 @@ int run()
|
|||||||
const coop::InputSnapshot input_snapshot = input_worker.snapshot();
|
const coop::InputSnapshot input_snapshot = input_worker.snapshot();
|
||||||
#ifdef COOP_WITH_STEAM
|
#ifdef COOP_WITH_STEAM
|
||||||
input_worker.set_want_steam(controllers.steam_input_requested());
|
input_worker.set_want_steam(controllers.steam_input_requested());
|
||||||
if (input_worker.steam_failed())
|
if (input_worker.steam_failed()) {
|
||||||
{
|
|
||||||
controllers.on_steam_init_failed(); // resets the toggle; worker falls back to XInput
|
controllers.on_steam_init_failed(); // resets the toggle; worker falls back to XInput
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
controllers.set_steam_active(input_snapshot.steam_active);
|
controllers.set_steam_active(input_snapshot.steam_active);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -422,58 +388,45 @@ int run()
|
|||||||
log.pull(injection); // drain hook log lines even while the Log window is hidden
|
log.pull(injection); // drain hook log lines even while the Log window is hidden
|
||||||
|
|
||||||
#ifdef COOP_TEST_HARNESS
|
#ifdef COOP_TEST_HARNESS
|
||||||
if (std::string tcmd = harness.poll_command(); !tcmd.empty())
|
if (std::string tcmd = harness.poll_command(); !tcmd.empty()) {
|
||||||
{
|
|
||||||
harness.write_response(apply_test_command(tcmd, ui, injection, audio, window));
|
harness.write_response(apply_test_command(tcmd, ui, injection, audio, window));
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_F1, false))
|
if (ImGui::IsKeyPressed(ImGuiKey_F1, false)) {
|
||||||
{
|
|
||||||
show_overlay = !show_overlay;
|
show_overlay = !show_overlay;
|
||||||
if (!show_overlay)
|
if (!show_overlay) {
|
||||||
{
|
|
||||||
overlay_hidden_at = ImGui::GetTime();
|
overlay_hidden_at = ImGui::GetTime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_F2, false))
|
if (ImGui::IsKeyPressed(ImGuiKey_F2, false)) {
|
||||||
{
|
|
||||||
injection.toggle_cursor_release(); // free/clip the operator's mouse for clipping games
|
injection.toggle_cursor_release(); // free/clip the operator's mouse for clipping games
|
||||||
}
|
}
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_F10, false))
|
if (ImGui::IsKeyPressed(ImGuiKey_F10, false)) {
|
||||||
{
|
|
||||||
window.request_screenshot(screenshot_path()); // captured at Present, overlay included
|
window.request_screenshot(screenshot_path()); // captured at Present, overlay included
|
||||||
}
|
}
|
||||||
|
|
||||||
coop::reset_panel_fit(); // panels record their overflow as they draw (UI-fit check)
|
coop::reset_panel_fit(); // panels record their overflow as they draw (UI-fit check)
|
||||||
coop::set_layout_debug(ui.debug_details); // center split adapts to the debug verbosity
|
coop::set_layout_debug(ui.debug_details); // center split adapts to the debug verbosity
|
||||||
if (show_overlay)
|
if (show_overlay) {
|
||||||
{
|
|
||||||
coop::draw_main_menu_bar(ui, stats);
|
coop::draw_main_menu_bar(ui, stats);
|
||||||
if (ui.show_controllers)
|
if (ui.show_controllers) {
|
||||||
{
|
|
||||||
controllers.draw(input_snapshot, injection.hook_status(), ui.debug_details);
|
controllers.draw(input_snapshot, injection.hook_status(), ui.debug_details);
|
||||||
}
|
}
|
||||||
if (ui.show_injection)
|
if (ui.show_injection) {
|
||||||
{
|
|
||||||
injection.draw(ui.debug_details);
|
injection.draw(ui.debug_details);
|
||||||
}
|
}
|
||||||
if (ui.show_audio)
|
if (ui.show_audio) {
|
||||||
{
|
|
||||||
audio.draw_ui(injection.hook_status(), ui.debug_details);
|
audio.draw_ui(injection.hook_status(), ui.debug_details);
|
||||||
}
|
}
|
||||||
if (ui.show_video)
|
if (ui.show_video) {
|
||||||
{
|
|
||||||
capture.draw_ui(stats);
|
capture.draw_ui(stats);
|
||||||
}
|
}
|
||||||
if (ui.show_log)
|
if (ui.show_log) {
|
||||||
{
|
|
||||||
log.draw();
|
log.draw();
|
||||||
}
|
}
|
||||||
draw_screenshot_toast(ImGui::GetTime() - last_shot_at, last_shot_name);
|
draw_screenshot_toast(ImGui::GetTime() - last_shot_at, last_shot_name);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
draw_overlay_hidden_hint(ImGui::GetTime() - overlay_hidden_at);
|
draw_overlay_hidden_hint(ImGui::GetTime() - overlay_hidden_at);
|
||||||
}
|
}
|
||||||
if (injection.hook_status().vk_too_late) // Vulkan game injected too late -> relaunch prompt
|
if (injection.hook_status().vk_too_late) // Vulkan game injected too late -> relaunch prompt
|
||||||
@@ -510,8 +463,7 @@ int run()
|
|||||||
// A host-side TDR / driver reset / GPU hang surfaces as a lost device on Present. We don't
|
// A host-side TDR / driver reset / GPU hang surfaces as a lost device on Present. We don't
|
||||||
// attempt to recreate the device (it would have to re-init ImGui + the capture pipeline);
|
// attempt to recreate the device (it would have to re-init ImGui + the capture pipeline);
|
||||||
// surface it and stop cleanly rather than spin forever rendering nothing.
|
// surface it and stop cleanly rather than spin forever rendering nothing.
|
||||||
if (window.device_lost())
|
if (window.device_lost()) {
|
||||||
{
|
|
||||||
wchar_t msg[320];
|
wchar_t msg[320];
|
||||||
swprintf_s(msg,
|
swprintf_s(msg,
|
||||||
L"The graphics device was lost (0x%08lX) -- a driver reset, GPU hang, or TDR on "
|
L"The graphics device was lost (0x%08lX) -- a driver reset, GPU hang, or TDR on "
|
||||||
@@ -523,8 +475,7 @@ int run()
|
|||||||
|
|
||||||
// render_frame saves a pending F10 screenshot just before Present; pick up the
|
// render_frame saves a pending F10 screenshot just before Present; pick up the
|
||||||
// result here so next frame shows the confirmation toast (kept out of the shot).
|
// result here so next frame shows the confirmation toast (kept out of the shot).
|
||||||
if (std::wstring shot = window.take_screenshot_result(); !shot.empty())
|
if (std::wstring shot = window.take_screenshot_result(); !shot.empty()) {
|
||||||
{
|
|
||||||
last_shot_at = ImGui::GetTime();
|
last_shot_at = ImGui::GetTime();
|
||||||
last_shot_name = screenshot_basename(shot);
|
last_shot_name = screenshot_basename(shot);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,8 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
namespace {
|
||||||
namespace
|
|
||||||
{
|
|
||||||
std::wstring temp_file(const wchar_t* name)
|
std::wstring temp_file(const wchar_t* name)
|
||||||
{
|
{
|
||||||
wchar_t dir[MAX_PATH] = {};
|
wchar_t dir[MAX_PATH] = {};
|
||||||
@@ -29,8 +27,7 @@ void TestHarness::init()
|
|||||||
std::string TestHarness::poll_command()
|
std::string TestHarness::poll_command()
|
||||||
{
|
{
|
||||||
std::ifstream f(cmd_path_.c_str()); // MSVC accepts a wide path
|
std::ifstream f(cmd_path_.c_str()); // MSVC accepts a wide path
|
||||||
if (!f)
|
if (!f) {
|
||||||
{
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
std::string line;
|
std::string line;
|
||||||
|
|||||||
@@ -12,11 +12,9 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
class TestHarness
|
class TestHarness {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
#ifdef COOP_TEST_HARNESS
|
#ifdef COOP_TEST_HARNESS
|
||||||
void init(); // resolve the %TEMP% file paths and clear any stale command
|
void init(); // resolve the %TEMP% file paths and clear any stale command
|
||||||
@@ -32,10 +30,7 @@ private:
|
|||||||
#else
|
#else
|
||||||
// No-op shims so call sites don't need their own #ifdef.
|
// No-op shims so call sites don't need their own #ifdef.
|
||||||
void init() {}
|
void init() {}
|
||||||
std::string poll_command()
|
std::string poll_command() { return {}; }
|
||||||
{
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
void write_response(const std::string&) {}
|
void write_response(const std::string&) {}
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,11 +8,9 @@
|
|||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
#include "imgui_internal.h" // ImGuiSettingsHandler / AddSettingsHandler (custom .ini section)
|
#include "imgui_internal.h" // ImGuiSettingsHandler / AddSettingsHandler (custom .ini section)
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
namespace {
|
||||||
{
|
|
||||||
// --- Custom .ini persistence for the UI switches ---------------------------
|
// --- Custom .ini persistence for the UI switches ---------------------------
|
||||||
// We piggy-back on ImGui's .ini so the "Debug details" verbosity survives restarts
|
// We piggy-back on ImGui's .ini so the "Debug details" verbosity survives restarts
|
||||||
// without inventing a separate settings file. The section looks like:
|
// without inventing a separate settings file. The section looks like:
|
||||||
@@ -33,8 +31,7 @@ void ui_settings_read_line(ImGuiContext*, ImGuiSettingsHandler*, void* entry, co
|
|||||||
auto* ui = static_cast<UiState*>(entry);
|
auto* ui = static_cast<UiState*>(entry);
|
||||||
// Manual parse (avoids the sscanf CRT-secure deprecation for a single int key).
|
// Manual parse (avoids the sscanf CRT-secure deprecation for a single int key).
|
||||||
constexpr char kKey[] = "DebugDetails=";
|
constexpr char kKey[] = "DebugDetails=";
|
||||||
if (std::strncmp(line, kKey, sizeof(kKey) - 1) == 0)
|
if (std::strncmp(line, kKey, sizeof(kKey) - 1) == 0) {
|
||||||
{
|
|
||||||
ui->debug_details = std::atoi(line + sizeof(kKey) - 1) != 0;
|
ui->debug_details = std::atoi(line + sizeof(kKey) - 1) != 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,8 +63,7 @@ float g_ref_h = 0.0f;
|
|||||||
bool g_layout_debug = false;
|
bool g_layout_debug = false;
|
||||||
|
|
||||||
// Per-frame panel-overflow registry (UI-fit instrumentation).
|
// Per-frame panel-overflow registry (UI-fit instrumentation).
|
||||||
struct PanelFit
|
struct PanelFit {
|
||||||
{
|
|
||||||
char name[24];
|
char name[24];
|
||||||
float over_x;
|
float over_x;
|
||||||
float over_y;
|
float over_y;
|
||||||
@@ -79,8 +75,7 @@ int g_fit_count = 0;
|
|||||||
void register_ui_settings(UiState& ui)
|
void register_ui_settings(UiState& ui)
|
||||||
{
|
{
|
||||||
// Idempotent: don't stack a second handler if this is somehow called twice.
|
// Idempotent: don't stack a second handler if this is somehow called twice.
|
||||||
if (ImGui::FindSettingsHandler(kUiSettingsType) != nullptr)
|
if (ImGui::FindSettingsHandler(kUiSettingsType) != nullptr) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ImGuiSettingsHandler handler;
|
ImGuiSettingsHandler handler;
|
||||||
@@ -106,8 +101,7 @@ void set_layout_persisted(bool had_persisted_layout)
|
|||||||
void apply_layout_end_frame()
|
void apply_layout_end_frame()
|
||||||
{
|
{
|
||||||
g_layout_reset = false;
|
g_layout_reset = false;
|
||||||
if (g_startup_force > 0)
|
if (g_startup_force > 0) {
|
||||||
{
|
|
||||||
--g_startup_force;
|
--g_startup_force;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,8 +129,7 @@ void record_panel_fit(const char* name)
|
|||||||
// so > 0 on either axis means content is cut off at the assigned size.
|
// so > 0 on either axis means content is cut off at the assigned size.
|
||||||
const float ox = ImGui::GetScrollMaxX();
|
const float ox = ImGui::GetScrollMaxX();
|
||||||
const float oy = ImGui::GetScrollMaxY();
|
const float oy = ImGui::GetScrollMaxY();
|
||||||
if (g_fit_count >= static_cast<int>(sizeof(g_fits) / sizeof(g_fits[0])))
|
if (g_fit_count >= static_cast<int>(sizeof(g_fits) / sizeof(g_fits[0]))) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PanelFit& f = g_fits[g_fit_count++];
|
PanelFit& f = g_fits[g_fit_count++];
|
||||||
@@ -148,17 +141,14 @@ void record_panel_fit(const char* name)
|
|||||||
bool panel_fit_overflow(float* worst_x, float* worst_y)
|
bool panel_fit_overflow(float* worst_x, float* worst_y)
|
||||||
{
|
{
|
||||||
float mx = 0.0f, my = 0.0f;
|
float mx = 0.0f, my = 0.0f;
|
||||||
for (int i = 0; i < g_fit_count; ++i)
|
for (int i = 0; i < g_fit_count; ++i) {
|
||||||
{
|
|
||||||
mx = std::max(mx, g_fits[i].over_x);
|
mx = std::max(mx, g_fits[i].over_x);
|
||||||
my = std::max(my, g_fits[i].over_y);
|
my = std::max(my, g_fits[i].over_y);
|
||||||
}
|
}
|
||||||
if (worst_x != nullptr)
|
if (worst_x != nullptr) {
|
||||||
{
|
|
||||||
*worst_x = mx;
|
*worst_x = mx;
|
||||||
}
|
}
|
||||||
if (worst_y != nullptr)
|
if (worst_y != nullptr) {
|
||||||
{
|
|
||||||
*worst_y = my;
|
*worst_y = my;
|
||||||
}
|
}
|
||||||
return mx > 0.5f || my > 0.5f;
|
return mx > 0.5f || my > 0.5f;
|
||||||
@@ -166,24 +156,20 @@ bool panel_fit_overflow(float* worst_x, float* worst_y)
|
|||||||
|
|
||||||
void panel_fit_report(char* buf, int cap)
|
void panel_fit_report(char* buf, int cap)
|
||||||
{
|
{
|
||||||
if (buf == nullptr || cap <= 0)
|
if (buf == nullptr || cap <= 0) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int n = 0;
|
int n = 0;
|
||||||
bool any = false;
|
bool any = false;
|
||||||
for (int i = 0; i < g_fit_count && n < cap - 1; ++i)
|
for (int i = 0; i < g_fit_count && n < cap - 1; ++i) {
|
||||||
{
|
if (g_fits[i].over_x <= 0.5f && g_fits[i].over_y <= 0.5f) {
|
||||||
if (g_fits[i].over_x <= 0.5f && g_fits[i].over_y <= 0.5f)
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
any = true;
|
any = true;
|
||||||
n += std::snprintf(buf + n, static_cast<size_t>(cap - n), "%s%s:%.0f,%.0f", n > 0 ? " " : "",
|
n += std::snprintf(buf + n, static_cast<size_t>(cap - n), "%s%s:%.0f,%.0f", n > 0 ? " " : "", g_fits[i].name,
|
||||||
g_fits[i].name, g_fits[i].over_x, g_fits[i].over_y);
|
g_fits[i].over_x, g_fits[i].over_y);
|
||||||
}
|
}
|
||||||
if (!any)
|
if (!any) {
|
||||||
{
|
|
||||||
std::snprintf(buf, static_cast<size_t>(cap), "fit");
|
std::snprintf(buf, static_cast<size_t>(cap), "fit");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,8 +213,7 @@ void apply_panel_layout(Panel panel)
|
|||||||
const float audio_h = stack_avail * audio_frac;
|
const float audio_h = stack_avail * audio_frac;
|
||||||
|
|
||||||
ImVec2 pos, size;
|
ImVec2 pos, size;
|
||||||
switch (panel)
|
switch (panel) {
|
||||||
{
|
|
||||||
case Panel::Injection:
|
case Panel::Injection:
|
||||||
pos = ImVec2(left_x, top);
|
pos = ImVec2(left_x, top);
|
||||||
size = ImVec2(left_w, full_h);
|
size = ImVec2(left_w, full_h);
|
||||||
@@ -255,8 +240,7 @@ void apply_panel_layout(Panel panel)
|
|||||||
// install with no saved layout to restore. Otherwise FirstUseEver lets ImGui's
|
// install with no saved layout to restore. Otherwise FirstUseEver lets ImGui's
|
||||||
// restored .ini positions stand (and still seeds any brand-new panel). A forced
|
// restored .ini positions stand (and still seeds any brand-new panel). A forced
|
||||||
// reference size (UI-fit check) also forces, so the assigned sizes are exact.
|
// reference size (UI-fit check) also forces, so the assigned sizes are exact.
|
||||||
const bool force =
|
const bool force = g_layout_reset || g_ref_w > 0.0f || (!g_had_persisted_layout && g_startup_force > 0);
|
||||||
g_layout_reset || g_ref_w > 0.0f || (!g_had_persisted_layout && g_startup_force > 0);
|
|
||||||
const ImGuiCond cond = force ? ImGuiCond_Always : ImGuiCond_FirstUseEver;
|
const ImGuiCond cond = force ? ImGuiCond_Always : ImGuiCond_FirstUseEver;
|
||||||
ImGui::SetNextWindowPos(pos, cond);
|
ImGui::SetNextWindowPos(pos, cond);
|
||||||
ImGui::SetNextWindowSize(size, cond);
|
ImGui::SetNextWindowSize(size, cond);
|
||||||
@@ -265,44 +249,37 @@ void apply_panel_layout(Panel panel)
|
|||||||
float draw_main_menu_bar(UiState& ui, const FrameStats& stats)
|
float draw_main_menu_bar(UiState& ui, const FrameStats& stats)
|
||||||
{
|
{
|
||||||
float height = 0.0f;
|
float height = 0.0f;
|
||||||
if (!ImGui::BeginMainMenuBar())
|
if (!ImGui::BeginMainMenuBar()) {
|
||||||
{
|
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TextUnformatted("CoopAllTheThings");
|
ImGui::TextUnformatted("CoopAllTheThings");
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
if (ImGui::BeginMenu("File"))
|
if (ImGui::BeginMenu("File")) {
|
||||||
{
|
if (ImGui::MenuItem("Exit", "Alt+F4")) {
|
||||||
if (ImGui::MenuItem("Exit", "Alt+F4"))
|
|
||||||
{
|
|
||||||
ui.request_quit = true; // the main loop sees this and stops
|
ui.request_quit = true; // the main loop sees this and stops
|
||||||
}
|
}
|
||||||
ImGui::EndMenu();
|
ImGui::EndMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui::BeginMenu("View"))
|
if (ImGui::BeginMenu("View")) {
|
||||||
{
|
|
||||||
ImGui::MenuItem("Controllers", nullptr, &ui.show_controllers);
|
ImGui::MenuItem("Controllers", nullptr, &ui.show_controllers);
|
||||||
ImGui::MenuItem("Injection", nullptr, &ui.show_injection);
|
ImGui::MenuItem("Injection", nullptr, &ui.show_injection);
|
||||||
ImGui::MenuItem("Video mirror", nullptr, &ui.show_video);
|
ImGui::MenuItem("Video mirror", nullptr, &ui.show_video);
|
||||||
ImGui::MenuItem("Audio mirror", nullptr, &ui.show_audio);
|
ImGui::MenuItem("Audio mirror", nullptr, &ui.show_audio);
|
||||||
ImGui::MenuItem("Log", nullptr, &ui.show_log);
|
ImGui::MenuItem("Log", nullptr, &ui.show_log);
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
if (ImGui::MenuItem("Debug details", nullptr, &ui.debug_details))
|
if (ImGui::MenuItem("Debug details", nullptr, &ui.debug_details)) {
|
||||||
{
|
|
||||||
ImGui::MarkIniSettingsDirty(); // persist the new verbosity to coop_layout.ini
|
ImGui::MarkIniSettingsDirty(); // persist the new verbosity to coop_layout.ini
|
||||||
}
|
}
|
||||||
if (ImGui::MenuItem("Reset layout"))
|
if (ImGui::MenuItem("Reset layout")) {
|
||||||
{
|
|
||||||
request_layout_reset();
|
request_layout_reset();
|
||||||
}
|
}
|
||||||
ImGui::EndMenu();
|
ImGui::EndMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui::BeginMenu("Help"))
|
if (ImGui::BeginMenu("Help")) {
|
||||||
{
|
|
||||||
ImGui::TextDisabled("F1 hide/show this overlay");
|
ImGui::TextDisabled("F1 hide/show this overlay");
|
||||||
ImGui::TextDisabled("F2 release/clip the operator cursor");
|
ImGui::TextDisabled("F2 release/clip the operator cursor");
|
||||||
ImGui::TextDisabled("F10 save a screenshot (PNG, next to the exe)");
|
ImGui::TextDisabled("F10 save a screenshot (PNG, next to the exe)");
|
||||||
@@ -318,16 +295,14 @@ float draw_main_menu_bar(UiState& ui, const FrameStats& stats)
|
|||||||
char perf[96];
|
char perf[96];
|
||||||
// Fixed field widths so the readout doesn't jitter/blur as values cross digit
|
// Fixed field widths so the readout doesn't jitter/blur as values cross digit
|
||||||
// thresholds (e.g. 99 -> 100) each frame.
|
// thresholds (e.g. 99 -> 100) each frame.
|
||||||
std::snprintf(perf, sizeof(perf), "%4.0f FPS %6.2f ms (%6.2f-%6.2f)", stats.fps(), stats.avg_ms(),
|
std::snprintf(perf, sizeof(perf), "%4.0f FPS %6.2f ms (%6.2f-%6.2f)", stats.fps(), stats.avg_ms(), stats.min_ms(),
|
||||||
stats.min_ms(), stats.max_ms());
|
stats.max_ms());
|
||||||
const float text_w = ImGui::CalcTextSize(perf).x;
|
const float text_w = ImGui::CalcTextSize(perf).x;
|
||||||
ImGui::SameLine(ImGui::GetWindowWidth() - text_w - ImGui::GetStyle().FramePadding.x * 2.0f);
|
ImGui::SameLine(ImGui::GetWindowWidth() - text_w - ImGui::GetStyle().FramePadding.x * 2.0f);
|
||||||
if (stats.max_ms() > 25.0f) // ~sub-40 FPS spike in the window
|
if (stats.max_ms() > 25.0f) // ~sub-40 FPS spike in the window
|
||||||
{
|
{
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", perf);
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", perf);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextUnformatted(perf);
|
ImGui::TextUnformatted(perf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,12 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// Visibility + verbosity shared by all panels. Panels read `debug_details` to
|
// Visibility + verbosity shared by all panels. Panels read `debug_details` to
|
||||||
// gate verbose diagnostics; the main loop reads the per-panel flags to decide
|
// gate verbose diagnostics; the main loop reads the per-panel flags to decide
|
||||||
// what to draw.
|
// what to draw.
|
||||||
struct UiState
|
struct UiState {
|
||||||
{
|
|
||||||
bool show_controllers = true;
|
bool show_controllers = true;
|
||||||
bool show_injection = true;
|
bool show_injection = true;
|
||||||
bool show_video = true;
|
bool show_video = true;
|
||||||
@@ -31,8 +29,7 @@ struct UiState
|
|||||||
void register_ui_settings(UiState& ui);
|
void register_ui_settings(UiState& ui);
|
||||||
|
|
||||||
// The overlay panels, for the shared default layout below.
|
// The overlay panels, for the shared default layout below.
|
||||||
enum class Panel
|
enum class Panel {
|
||||||
{
|
|
||||||
Injection, // left column, full height (room for hook diagnostics)
|
Injection, // left column, full height (room for hook diagnostics)
|
||||||
Controllers, // center column, top
|
Controllers, // center column, top
|
||||||
Video, // center column, below Controllers
|
Video, // center column, below Controllers
|
||||||
@@ -89,26 +86,22 @@ void panel_fit_report(char* buf, int cap);
|
|||||||
|
|
||||||
// Rolling frame-timing over a ~1 s window, recomputed each window so the status
|
// Rolling frame-timing over a ~1 s window, recomputed each window so the status
|
||||||
// bar can show a stable FPS plus the min/max frame time (jitter) underneath it.
|
// bar can show a stable FPS plus the min/max frame time (jitter) underneath it.
|
||||||
class FrameStats
|
class FrameStats {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
// Number of frame samples kept for the graphs (~2 s at 120 FPS).
|
// Number of frame samples kept for the graphs (~2 s at 120 FPS).
|
||||||
static constexpr int kHistory = 240;
|
static constexpr int kHistory = 240;
|
||||||
|
|
||||||
void tick(float dt_ms)
|
void tick(float dt_ms)
|
||||||
{
|
{
|
||||||
if (dt_ms < cur_min_)
|
if (dt_ms < cur_min_) {
|
||||||
{
|
|
||||||
cur_min_ = dt_ms;
|
cur_min_ = dt_ms;
|
||||||
}
|
}
|
||||||
if (dt_ms > cur_max_)
|
if (dt_ms > cur_max_) {
|
||||||
{
|
|
||||||
cur_max_ = dt_ms;
|
cur_max_ = dt_ms;
|
||||||
}
|
}
|
||||||
accum_ms_ += dt_ms;
|
accum_ms_ += dt_ms;
|
||||||
++frames_;
|
++frames_;
|
||||||
if (accum_ms_ >= 1000.0f && frames_ > 0)
|
if (accum_ms_ >= 1000.0f && frames_ > 0) {
|
||||||
{
|
|
||||||
avg_ms_ = accum_ms_ / static_cast<float>(frames_);
|
avg_ms_ = accum_ms_ / static_cast<float>(frames_);
|
||||||
min_ms_ = cur_min_;
|
min_ms_ = cur_min_;
|
||||||
max_ms_ = cur_max_;
|
max_ms_ = cur_max_;
|
||||||
@@ -120,43 +113,26 @@ public:
|
|||||||
|
|
||||||
history_[hist_pos_] = dt_ms;
|
history_[hist_pos_] = dt_ms;
|
||||||
hist_pos_ = (hist_pos_ + 1) % kHistory;
|
hist_pos_ = (hist_pos_ + 1) % kHistory;
|
||||||
if (hist_count_ < kHistory)
|
if (hist_count_ < kHistory) {
|
||||||
{
|
|
||||||
++hist_count_;
|
++hist_count_;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 1 s windowed aggregates (stable readout for the menu bar) ---------
|
// --- 1 s windowed aggregates (stable readout for the menu bar) ---------
|
||||||
[[nodiscard]] float avg_ms() const
|
[[nodiscard]] float avg_ms() const { return avg_ms_; }
|
||||||
{
|
[[nodiscard]] float min_ms() const { return min_ms_; }
|
||||||
return avg_ms_;
|
[[nodiscard]] float max_ms() const { return max_ms_; }
|
||||||
}
|
[[nodiscard]] float fps() const { return avg_ms_ > 0.0f ? 1000.0f / avg_ms_ : 0.0f; }
|
||||||
[[nodiscard]] float min_ms() const
|
|
||||||
{
|
|
||||||
return min_ms_;
|
|
||||||
}
|
|
||||||
[[nodiscard]] float max_ms() const
|
|
||||||
{
|
|
||||||
return max_ms_;
|
|
||||||
}
|
|
||||||
[[nodiscard]] float fps() const
|
|
||||||
{
|
|
||||||
return avg_ms_ > 0.0f ? 1000.0f / avg_ms_ : 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Sample history (for graphs) ---------------------------------------
|
// --- Sample history (for graphs) ---------------------------------------
|
||||||
[[nodiscard]] int history_size() const
|
[[nodiscard]] int history_size() const { return hist_count_; }
|
||||||
{
|
|
||||||
return hist_count_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the frame-time samples (ms) into `out` oldest-to-newest; `out` must
|
// Copy the frame-time samples (ms) into `out` oldest-to-newest; `out` must
|
||||||
// hold at least kHistory floats. Returns the number written.
|
// hold at least kHistory floats. Returns the number written.
|
||||||
int copy_frame_ms(float* out) const
|
int copy_frame_ms(float* out) const
|
||||||
{
|
{
|
||||||
const int start = (hist_pos_ - hist_count_ + kHistory * 2) % kHistory;
|
const int start = (hist_pos_ - hist_count_ + kHistory * 2) % kHistory;
|
||||||
for (int i = 0; i < hist_count_; ++i)
|
for (int i = 0; i < hist_count_; ++i) {
|
||||||
{
|
|
||||||
out[i] = history_[(start + i) % kHistory];
|
out[i] = history_[(start + i) % kHistory];
|
||||||
}
|
}
|
||||||
return hist_count_;
|
return hist_count_;
|
||||||
@@ -165,14 +141,12 @@ public:
|
|||||||
// min / max / mean over the whole retained history (order-independent).
|
// min / max / mean over the whole retained history (order-independent).
|
||||||
void history_stats(float& min_ms, float& max_ms, float& avg_ms) const
|
void history_stats(float& min_ms, float& max_ms, float& avg_ms) const
|
||||||
{
|
{
|
||||||
if (hist_count_ == 0)
|
if (hist_count_ == 0) {
|
||||||
{
|
|
||||||
min_ms = max_ms = avg_ms = 0.0f;
|
min_ms = max_ms = avg_ms = 0.0f;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
float mn = 1.0e9f, mx = 0.0f, sum = 0.0f;
|
float mn = 1.0e9f, mx = 0.0f, sum = 0.0f;
|
||||||
for (int i = 0; i < hist_count_; ++i)
|
for (int i = 0; i < hist_count_; ++i) {
|
||||||
{
|
|
||||||
const float v = history_[i];
|
const float v = history_[i];
|
||||||
mn = std::min(mn, v);
|
mn = std::min(mn, v);
|
||||||
mx = std::max(mx, v);
|
mx = std::max(mx, v);
|
||||||
|
|||||||
@@ -6,13 +6,11 @@
|
|||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
inline std::string ascii_lower(std::string s)
|
inline std::string ascii_lower(std::string s)
|
||||||
{
|
{
|
||||||
for (char& c : s)
|
for (char& c : s) {
|
||||||
{
|
|
||||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
@@ -21,8 +19,7 @@ inline std::string ascii_lower(std::string s)
|
|||||||
// True when `needle` is empty or a case-insensitive substring of `haystack`.
|
// True when `needle` is empty or a case-insensitive substring of `haystack`.
|
||||||
inline bool contains_ci(const std::string& haystack, const char* needle)
|
inline bool contains_ci(const std::string& haystack, const char* needle)
|
||||||
{
|
{
|
||||||
if (needle == nullptr || needle[0] == '\0')
|
if (needle == nullptr || needle[0] == '\0') {
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return ascii_lower(haystack).find(ascii_lower(needle)) != std::string::npos;
|
return ascii_lower(haystack).find(ascii_lower(needle)) != std::string::npos;
|
||||||
|
|||||||
@@ -6,14 +6,12 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
|
||||||
|
|
||||||
// UTF-16 -> UTF-8.
|
// UTF-16 -> UTF-8.
|
||||||
inline std::string narrow(const std::wstring& w)
|
inline std::string narrow(const std::wstring& w)
|
||||||
{
|
{
|
||||||
if (w.empty())
|
if (w.empty()) {
|
||||||
{
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(w.size()), nullptr, 0, nullptr, nullptr);
|
const int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(w.size()), nullptr, 0, nullptr, nullptr);
|
||||||
@@ -25,8 +23,7 @@ inline std::string narrow(const std::wstring& w)
|
|||||||
// UTF-8 -> UTF-16.
|
// UTF-8 -> UTF-16.
|
||||||
inline std::wstring widen(const std::string& s)
|
inline std::wstring widen(const std::string& s)
|
||||||
{
|
{
|
||||||
if (s.empty())
|
if (s.empty()) {
|
||||||
{
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), nullptr, 0);
|
const int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), nullptr, 0);
|
||||||
|
|||||||
@@ -5,10 +5,8 @@
|
|||||||
#include "coop/tool_paths.hpp"
|
#include "coop/tool_paths.hpp"
|
||||||
#include "util/utf8.hpp"
|
#include "util/utf8.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop {
|
||||||
{
|
namespace {
|
||||||
namespace
|
|
||||||
{
|
|
||||||
// The Vulkan loader's per-user implicit-layer registry list. Each value is a manifest path; its
|
// The Vulkan loader's per-user implicit-layer registry list. Each value is a manifest path; its
|
||||||
// DWORD data 0 = enabled.
|
// DWORD data 0 = enabled.
|
||||||
constexpr const wchar_t* kImplicitLayersKey = L"SOFTWARE\\Khronos\\Vulkan\\ImplicitLayers";
|
constexpr const wchar_t* kImplicitLayersKey = L"SOFTWARE\\Khronos\\Vulkan\\ImplicitLayers";
|
||||||
@@ -30,14 +28,12 @@ bool register_vk_layer(const std::wstring& target_image)
|
|||||||
{
|
{
|
||||||
// Write the scoping file (target image basename, UTF-8) the layer checks against its own image.
|
// Write the scoping file (target image basename, UTF-8) the layer checks against its own image.
|
||||||
const std::wstring sf = scoping_file();
|
const std::wstring sf = scoping_file();
|
||||||
if (!sf.empty())
|
if (!sf.empty()) {
|
||||||
{
|
|
||||||
const std::size_t slash = target_image.find_last_of(L"\\/");
|
const std::size_t slash = target_image.find_last_of(L"\\/");
|
||||||
const std::wstring base = slash == std::wstring::npos ? target_image : target_image.substr(slash + 1);
|
const std::wstring base = slash == std::wstring::npos ? target_image : target_image.substr(slash + 1);
|
||||||
const std::string utf8 = narrow(base);
|
const std::string utf8 = narrow(base);
|
||||||
HANDLE f = CreateFileW(sf.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
HANDLE f = CreateFileW(sf.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||||
if (f != INVALID_HANDLE_VALUE)
|
if (f != INVALID_HANDLE_VALUE) {
|
||||||
{
|
|
||||||
DWORD written = 0;
|
DWORD written = 0;
|
||||||
WriteFile(f, utf8.data(), static_cast<DWORD>(utf8.size()), &written, nullptr);
|
WriteFile(f, utf8.data(), static_cast<DWORD>(utf8.size()), &written, nullptr);
|
||||||
CloseHandle(f);
|
CloseHandle(f);
|
||||||
@@ -45,15 +41,14 @@ bool register_vk_layer(const std::wstring& target_image)
|
|||||||
}
|
}
|
||||||
|
|
||||||
HKEY key = nullptr;
|
HKEY key = nullptr;
|
||||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, kImplicitLayersKey, 0, nullptr, 0, KEY_SET_VALUE, nullptr, &key,
|
if (RegCreateKeyExW(HKEY_CURRENT_USER, kImplicitLayersKey, 0, nullptr, 0, KEY_SET_VALUE, nullptr, &key, nullptr)
|
||||||
nullptr) != ERROR_SUCCESS)
|
!= ERROR_SUCCESS) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const std::wstring mp = manifest_path();
|
const std::wstring mp = manifest_path();
|
||||||
DWORD enabled = 0; // 0 = enabled, per the loader's convention
|
DWORD enabled = 0; // 0 = enabled, per the loader's convention
|
||||||
const LONG r = RegSetValueExW(key, mp.c_str(), 0, REG_DWORD, reinterpret_cast<const BYTE*>(&enabled),
|
const LONG r =
|
||||||
sizeof(enabled));
|
RegSetValueExW(key, mp.c_str(), 0, REG_DWORD, reinterpret_cast<const BYTE*>(&enabled), sizeof(enabled));
|
||||||
RegCloseKey(key);
|
RegCloseKey(key);
|
||||||
return r == ERROR_SUCCESS;
|
return r == ERROR_SUCCESS;
|
||||||
}
|
}
|
||||||
@@ -61,14 +56,12 @@ bool register_vk_layer(const std::wstring& target_image)
|
|||||||
void unregister_vk_layer()
|
void unregister_vk_layer()
|
||||||
{
|
{
|
||||||
HKEY key = nullptr;
|
HKEY key = nullptr;
|
||||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, kImplicitLayersKey, 0, KEY_SET_VALUE, &key) == ERROR_SUCCESS)
|
if (RegOpenKeyExW(HKEY_CURRENT_USER, kImplicitLayersKey, 0, KEY_SET_VALUE, &key) == ERROR_SUCCESS) {
|
||||||
{
|
|
||||||
RegDeleteValueW(key, manifest_path().c_str());
|
RegDeleteValueW(key, manifest_path().c_str());
|
||||||
RegCloseKey(key);
|
RegCloseKey(key);
|
||||||
}
|
}
|
||||||
const std::wstring sf = scoping_file();
|
const std::wstring sf = scoping_file();
|
||||||
if (!sf.empty())
|
if (!sf.empty()) {
|
||||||
{
|
|
||||||
DeleteFileW(sf.c_str());
|
DeleteFileW(sf.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user