Add native GPU static layout export path

This commit is contained in:
wiiiii123
2026-05-03 22:43:23 +07:00
parent 816116de26
commit 9c81006e5c
62 changed files with 9819 additions and 328 deletions
Binary file not shown.
@@ -5,17 +5,24 @@
"helpers": {
"wgc-capture": {
"binaryName": "wgc-capture.exe",
"binarySha256": "bb4c2aa4141e81e1a05b54bd49dbb114eb3a5ff4a666bf7d26525a9da585128b",
"binarySha256": "424c64fc9569e8f6add29130ee935dfcb4451c07c1036d3738c22d9b79ad39cd",
"sourceDir": "electron/native/wgc-capture",
"sourceFingerprint": "5bb02a69049a02909d38188cae1dfdfb94fd49465b51ed6ab78a98092b7520cc",
"updatedAt": "2026-04-25T09:17:16.237Z"
"sourceFingerprint": "4fd7e2f5d0e804a8aaaaddcba3a3e02b42739c0148a0d38be24325ddbb659ec1",
"updatedAt": "2026-05-03T15:39:44.054Z"
},
"cursor-monitor": {
"binaryName": "cursor-monitor.exe",
"binarySha256": "b0732abc06998a40c3e95078465ad750a6169901944571c39cfd7996effe39c0",
"binarySha256": "6ae6d91103b6e891a851e8ea5791e1c1f9aaab700134c18bc4c46cfffd7fdd12",
"sourceDir": "electron/native/cursor-monitor",
"sourceFingerprint": "6ad1b8b50bb336f2a48937b06f5ec56d90b6ab4a3e56a4bca278cf67a5d3e52e",
"updatedAt": "2026-03-29T02:15:38.286Z"
"updatedAt": "2026-05-03T15:39:52.446Z"
},
"recordly-gpu-export": {
"binaryName": "recordly-gpu-export.exe",
"binarySha256": "9b3d4dff520356e5db563cc3992d777f8bab8eaf6d0bb718f9c1d7d7da37fac5",
"sourceDir": "electron/native/gpu-export-probe",
"sourceFingerprint": "75bf080c4a5cbbcb1d42fb088a1545130c613e42d9e7681cc23f9151d1c8072b",
"updatedAt": "2026-05-03T15:39:47.938Z"
}
}
}
Binary file not shown.
@@ -0,0 +1,37 @@
cmake_minimum_required(VERSION 3.20)
project(gpu-export-probe LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(gpu-export-probe
src/main.cpp
)
set(NVIDIA_VIDEO_SDK_SAMPLES_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../.tmp/video-sdk-samples/Samples")
set(NVIDIA_NVENC_SDK_DIR "${NVIDIA_VIDEO_SDK_SAMPLES_DIR}/NvCodec")
if(EXISTS "${NVIDIA_NVENC_SDK_DIR}/NvEncoder/NvEncoderD3D11.cpp")
target_sources(gpu-export-probe PRIVATE
"${NVIDIA_NVENC_SDK_DIR}/NvEncoder/NvEncoder.cpp"
"${NVIDIA_NVENC_SDK_DIR}/NvEncoder/NvEncoderD3D11.cpp"
)
target_include_directories(gpu-export-probe PRIVATE
"${NVIDIA_VIDEO_SDK_SAMPLES_DIR}"
"${NVIDIA_NVENC_SDK_DIR}"
)
target_compile_definitions(gpu-export-probe PRIVATE RECORDLY_GPU_EXPORT_ENABLE_NVENC_SDK=1)
endif()
target_compile_options(gpu-export-probe PRIVATE /EHsc /W4 /utf-8)
target_link_libraries(gpu-export-probe PRIVATE
d3d11
d3dcompiler
dxgi
mfplat
mfreadwrite
mf
mfuuid
ole32
windowscodecs
)
File diff suppressed because it is too large Load Diff
+2
View File
@@ -183,6 +183,8 @@ static void writeCompanionAudioTimingMetadata(
}
metadataFile << "{\"startDelayMs\":" << startDelayMs;
metadataFile << ",\"capturedDurationMs\":" << capture.capturedDurationMs();
metadataFile << ",\"dataBytes\":" << capture.totalDataBytes();
const uint32_t discontinuityCount = capture.dataDiscontinuityCount();
if (discontinuityCount > 0) {
metadataFile << ",\"dataDiscontinuityCount\":" << discontinuityCount;
@@ -2,9 +2,41 @@
#include <functiondiscoverykeys_devpkey.h>
#include <iostream>
#include <cstring>
#include <algorithm>
#pragma comment(lib, "ole32.lib")
namespace {
constexpr int64_t kHundredNanosecondsPerSecond = 10000000;
constexpr uint64_t kSilenceWriteChunkFrames = 4096;
bool isFloatFormat(const WAVEFORMATEX* format) {
if (!format) return false;
if (format->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) return true;
if (format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) return false;
return reinterpret_cast<const WAVEFORMATEXTENSIBLE*>(format)->SubFormat ==
KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
}
bool isPcmFormat(const WAVEFORMATEX* format) {
if (!format) return false;
if (format->wFormatTag == WAVE_FORMAT_PCM) return true;
if (format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) return false;
return reinterpret_cast<const WAVEFORMATEXTENSIBLE*>(format)->SubFormat ==
KSDATAFORMAT_SUBTYPE_PCM;
}
int16_t pcm24ToInt16(const BYTE* sample) {
int32_t value = static_cast<int32_t>(sample[0]) |
(static_cast<int32_t>(sample[1]) << 8) |
(static_cast<int32_t>(sample[2]) << 16);
if ((value & 0x800000) != 0) {
value |= ~0xFFFFFF;
}
return static_cast<int16_t>(value >> 8);
}
}
static const CLSID CLSID_MMDeviceEnumerator_ = __uuidof(MMDeviceEnumerator);
static const IID IID_IMMDeviceEnumerator_ = __uuidof(IMMDeviceEnumerator);
static const IID IID_IAudioClient_ = __uuidof(IAudioClient);
@@ -137,6 +169,7 @@ bool WasapiCapture::start() {
}
totalDataBytes_ = 0;
framesWritten_ = 0;
firstPacketQpcHns_ = -1;
dataDiscontinuityCount_ = 0;
timestampErrorCount_ = 0;
@@ -177,7 +210,10 @@ void WasapiCapture::stop() {
if (outputFile_ != INVALID_HANDLE_VALUE) {
SetFilePointer(outputFile_, 0, nullptr, FILE_BEGIN);
writeWavHeader(outputFile_, totalDataBytes_);
const uint64_t dataBytes = totalDataBytes_.load();
const DWORD wavDataBytes =
static_cast<DWORD>(std::min<uint64_t>(dataBytes, 0xFFFFFFFFu));
writeWavHeader(outputFile_, wavDataBytes);
CloseHandle(outputFile_);
outputFile_ = INVALID_HANDLE_VALUE;
}
@@ -217,6 +253,39 @@ bool WasapiCapture::writeWavHeader(HANDLE file, DWORD dataSize) {
return true;
}
void WasapiCapture::writePcmFrames(const int16_t* samples, UINT32 frameCount, WORD channels) {
if (!samples || frameCount == 0 || channels == 0 || outputFile_ == INVALID_HANDLE_VALUE) {
return;
}
const DWORD bytesToWrite = frameCount * channels * sizeof(int16_t);
DWORD written = 0;
WriteFile(outputFile_, samples, bytesToWrite, &written, nullptr);
totalDataBytes_.fetch_add(written);
framesWritten_.fetch_add(written / (channels * sizeof(int16_t)));
}
void WasapiCapture::writeSilenceFrames(uint64_t frameCount, WORD channels) {
if (frameCount == 0 || channels == 0 || outputFile_ == INVALID_HANDLE_VALUE) {
return;
}
std::vector<int16_t> silence(static_cast<size_t>(kSilenceWriteChunkFrames * channels), 0);
while (frameCount > 0) {
const uint64_t chunkFrames = std::min<uint64_t>(frameCount, kSilenceWriteChunkFrames);
writePcmFrames(silence.data(), static_cast<UINT32>(chunkFrames), channels);
frameCount -= chunkFrames;
}
}
uint64_t WasapiCapture::capturedDurationMs() const {
if (!mixFormat_ || mixFormat_->nSamplesPerSec == 0) {
return 0;
}
return (framesWritten_.load() * 1000) / mixFormat_->nSamplesPerSec;
}
void WasapiCapture::captureThread() {
// COM must be initialized on every thread that uses COM objects.
// The main thread calls winrt::init_apartment(MTA) but that only covers
@@ -225,9 +294,12 @@ void WasapiCapture::captureThread() {
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
WORD channels = static_cast<WORD>(mixFormat_->nChannels);
bool isFloat = (mixFormat_->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) ||
(mixFormat_->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(mixFormat_)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT);
const bool isFloat = isFloatFormat(mixFormat_);
const bool isPcm = isPcmFormat(mixFormat_);
const WORD bitsPerSample = mixFormat_->wBitsPerSample;
const WORD sourceBlockAlign = mixFormat_->nBlockAlign;
const WORD sourceBytesPerSample =
channels > 0 ? static_cast<WORD>(sourceBlockAlign / channels) : 0;
std::vector<int16_t> pcmBuffer;
@@ -273,38 +345,96 @@ void WasapiCapture::captureThread() {
if ((flags & AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR) != 0) {
timestampErrorCount_.fetch_add(1);
}
if (
const bool hasReliableTimestamp =
numFrames > 0 &&
qpcPosition > 0 &&
(flags & AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR) == 0
) {
(flags & AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR) == 0;
if (hasReliableTimestamp) {
int64_t expected = -1;
firstPacketQpcHns_.compare_exchange_strong(
expected,
static_cast<int64_t>(qpcPosition));
const int64_t firstPacketQpcHns = firstPacketQpcHns_.load();
if (
firstPacketQpcHns >= 0 &&
static_cast<int64_t>(qpcPosition) > firstPacketQpcHns
) {
const int64_t elapsedHns =
static_cast<int64_t>(qpcPosition) - firstPacketQpcHns;
const uint64_t expectedStartFrame =
(static_cast<uint64_t>(elapsedHns) * mixFormat_->nSamplesPerSec +
kHundredNanosecondsPerSecond / 2) /
kHundredNanosecondsPerSecond;
const uint64_t writtenFrames = framesWritten_.load();
const uint64_t gapThresholdFrames = mixFormat_->nSamplesPerSec / 100;
if (expectedStartFrame > writtenFrames + gapThresholdFrames) {
writeSilenceFrames(expectedStartFrame - writtenFrames, channels);
}
}
}
UINT32 totalSamples = numFrames * channels;
if (flags & AUDCLNT_BUFFERFLAGS_SILENT) {
pcmBuffer.assign(totalSamples, 0);
} else if (isFloat) {
} else if (isFloat && bitsPerSample == 32 && sourceBytesPerSample >= 4) {
pcmBuffer.resize(totalSamples);
const float* src = reinterpret_cast<const float*>(data);
for (UINT32 i = 0; i < totalSamples; i++) {
pcmBuffer[i] = floatToInt16(src[i]);
}
} else {
} else if (isPcm && bitsPerSample == 16 && sourceBytesPerSample >= 2) {
pcmBuffer.resize(totalSamples);
std::memcpy(pcmBuffer.data(), data, totalSamples * sizeof(int16_t));
if (sourceBytesPerSample == sizeof(int16_t)) {
std::memcpy(pcmBuffer.data(), data, totalSamples * sizeof(int16_t));
} else {
for (UINT32 frame = 0; frame < numFrames; frame++) {
const BYTE* frameData = data + frame * sourceBlockAlign;
for (WORD channel = 0; channel < channels; channel++) {
const BYTE* sample = frameData + channel * sourceBytesPerSample;
pcmBuffer[frame * channels + channel] =
*reinterpret_cast<const int16_t*>(sample);
}
}
}
} else if (isPcm && bitsPerSample == 24 && sourceBytesPerSample >= 3) {
pcmBuffer.resize(totalSamples);
for (UINT32 frame = 0; frame < numFrames; frame++) {
const BYTE* frameData = data + frame * sourceBlockAlign;
for (WORD channel = 0; channel < channels; channel++) {
const BYTE* sample = frameData + channel * sourceBytesPerSample;
pcmBuffer[frame * channels + channel] = pcm24ToInt16(sample);
}
}
} else if (isPcm && bitsPerSample == 32 && sourceBytesPerSample >= 4) {
pcmBuffer.resize(totalSamples);
for (UINT32 frame = 0; frame < numFrames; frame++) {
const BYTE* frameData = data + frame * sourceBlockAlign;
for (WORD channel = 0; channel < channels; channel++) {
const BYTE* sample = frameData + channel * sourceBytesPerSample;
const int32_t value = *reinterpret_cast<const int32_t*>(sample);
pcmBuffer[frame * channels + channel] = static_cast<int16_t>(value >> 16);
}
}
} else if (isPcm && bitsPerSample == 8 && sourceBytesPerSample >= 1) {
pcmBuffer.resize(totalSamples);
for (UINT32 frame = 0; frame < numFrames; frame++) {
const BYTE* frameData = data + frame * sourceBlockAlign;
for (WORD channel = 0; channel < channels; channel++) {
const BYTE value = *(frameData + channel * sourceBytesPerSample);
pcmBuffer[frame * channels + channel] =
static_cast<int16_t>((static_cast<int>(value) - 128) << 8);
}
}
} else {
pcmBuffer.assign(totalSamples, 0);
}
captureClient_->ReleaseBuffer(numFrames);
DWORD bytesToWrite = totalSamples * sizeof(int16_t);
DWORD written;
WriteFile(outputFile_, pcmBuffer.data(), bytesToWrite, &written, nullptr);
totalDataBytes_ += written;
writePcmFrames(pcmBuffer.data(), numFrames, channels);
hr = captureClient_->GetNextPacketSize(&packetLength);
if (FAILED(hr)) {
@@ -20,6 +20,8 @@ public:
bool resume();
void stop();
int64_t firstPacketQpcHns() const { return firstPacketQpcHns_.load(); }
uint64_t capturedDurationMs() const;
uint64_t totalDataBytes() const { return totalDataBytes_.load(); }
uint32_t dataDiscontinuityCount() const { return dataDiscontinuityCount_.load(); }
uint32_t timestampErrorCount() const { return timestampErrorCount_.load(); }
@@ -27,6 +29,8 @@ private:
bool initializeCommon();
void captureThread();
bool writeWavHeader(HANDLE file, DWORD dataSize);
void writePcmFrames(const int16_t* samples, UINT32 frameCount, WORD channels);
void writeSilenceFrames(uint64_t frameCount, WORD channels);
IMMDevice* findCaptureDeviceByName(const std::wstring& name);
std::string outputPath_;
@@ -34,7 +38,8 @@ private:
std::atomic<bool> capturing_{false};
std::atomic<bool> paused_{false};
HANDLE outputFile_ = INVALID_HANDLE_VALUE;
DWORD totalDataBytes_ = 0;
std::atomic<uint64_t> totalDataBytes_{0};
std::atomic<uint64_t> framesWritten_{0};
IMMDeviceEnumerator* enumerator_ = nullptr;
IMMDevice* device_ = nullptr;