From 94bbd0280f71bd1937703934d29e7def612530c9 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Mon, 4 May 2026 09:59:41 +0700 Subject: [PATCH] Add NVIDIA CUDA export compositor --- .gitignore | 1 + electron/ipc/export/native-video.test.ts | 132 + electron/ipc/export/native-video.ts | 214 +- electron/ipc/register/recording.ts | 14 +- .../bin/win32-x64/helpers-manifest.json | 7 + .../recordly-nvidia-cuda-compositor.exe | Bin 0 -> 451584 bytes .../nvidia-cuda-compositor/CMakeLists.txt | 47 + .../render-tahoe-cursor-atlas.cjs | 144 + .../run-mp4-pipeline.mjs | 1104 ++++++ .../native/nvidia-cuda-compositor/src/main.cu | 3006 +++++++++++++++++ package.json | 3 +- scripts/build-nvidia-cuda-compositor.mjs | 193 ++ scripts/launch-recordly-cuda-auto.ps1 | 80 + scripts/smoke-packaged-binaries.mjs | 5 + src/hooks/useScreenRecorder.ts | 17 +- src/lib/exporter/modernVideoExporter.ts | 8 +- 16 files changed, 4950 insertions(+), 25 deletions(-) create mode 100644 electron/native/bin/win32-x64/recordly-nvidia-cuda-compositor.exe create mode 100644 electron/native/nvidia-cuda-compositor/CMakeLists.txt create mode 100644 electron/native/nvidia-cuda-compositor/render-tahoe-cursor-atlas.cjs create mode 100644 electron/native/nvidia-cuda-compositor/run-mp4-pipeline.mjs create mode 100644 electron/native/nvidia-cuda-compositor/src/main.cu create mode 100644 scripts/build-nvidia-cuda-compositor.mjs create mode 100644 scripts/launch-recordly-cuda-auto.ps1 diff --git a/.gitignore b/.gitignore index 605ab2f5..3784c0cf 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ vite.config.d.ts electron/native/wgc-capture/build/ electron/native/cursor-monitor/build/ electron/native/gpu-export-probe/build/ +electron/native/nvidia-cuda-compositor/build/ electron/native/bin/*/whisper-* electron/native/bin/*/whisper-runtime.json diff --git a/electron/ipc/export/native-video.test.ts b/electron/ipc/export/native-video.test.ts index 3e004f1b..91f11c01 100644 --- a/electron/ipc/export/native-video.test.ts +++ b/electron/ipc/export/native-video.test.ts @@ -14,6 +14,7 @@ vi.mock("../ffmpeg/binary", () => ({ import { buildNativeVideoAudioMuxArgs, + getNvidiaCudaAudioExportSkipReason, normalizeNativeStaticLayoutBackground, parseFfmpegDurationSeconds, parseFfmpegFrameRate, @@ -21,8 +22,29 @@ import { parseNvidiaCudaExportSummary, parseWindowsGpuExportProgressLine, parseWindowsGpuExportSummary, + validateNvidiaCudaExportSummary, } from "./native-video"; +function withNvidiaCudaAudioOverride(value: string | undefined, callback: () => T) { + const envName = "RECORDLY_NVIDIA_CUDA_ALLOW_AUDIO_EXPORT"; + const originalValue = process.env[envName]; + if (value === undefined) { + delete process.env[envName]; + } else { + process.env[envName] = value; + } + + try { + return callback(); + } finally { + if (originalValue === undefined) { + delete process.env[envName]; + } else { + process.env[envName] = originalValue; + } + } +} + describe("normalizeNativeStaticLayoutBackground", () => { it("falls back to a solid background when the configured image file is missing", async () => { const normalized = await normalizeNativeStaticLayoutBackground({ @@ -46,6 +68,35 @@ describe("normalizeNativeStaticLayoutBackground", () => { }); }); +describe("getNvidiaCudaAudioExportSkipReason", () => { + it("allows video-only CUDA exports by default", () => { + withNvidiaCudaAudioOverride(undefined, () => { + expect(getNvidiaCudaAudioExportSkipReason(undefined)).toBeNull(); + expect(getNvidiaCudaAudioExportSkipReason("none")).toBeNull(); + }); + }); + + it("guards CUDA for audio exports unless explicitly overridden", () => { + withNvidiaCudaAudioOverride(undefined, () => { + expect(getNvidiaCudaAudioExportSkipReason("copy-source")).toBe( + "audio-mode:copy-source", + ); + expect(getNvidiaCudaAudioExportSkipReason("trim-source")).toBe( + "audio-mode:trim-source", + ); + expect(getNvidiaCudaAudioExportSkipReason("edited-track")).toBe( + "audio-mode:edited-track", + ); + }); + }); + + it("allows CUDA audio exports only for explicit lab overrides", () => { + withNvidiaCudaAudioOverride("1", () => { + expect(getNvidiaCudaAudioExportSkipReason("copy-source")).toBeNull(); + }); + }); +}); + describe("buildNativeVideoAudioMuxArgs", () => { it("stream-copies source audio and preserves the requested video duration", () => { const args = buildNativeVideoAudioMuxArgs("video.mp4", "source.mp4", "out.mp4", { @@ -151,6 +202,87 @@ describe("parseNvidiaCudaExportSummary", () => { }); }); +describe("validateNvidiaCudaExportSummary", () => { + it("accepts CUDA output when frames and stream durations match the export target", () => { + const issues = validateNvidiaCudaExportSummary( + { + success: true, + targetFrames: 300, + durationSec: 10, + nativeSummary: { success: true, frames: 300 }, + outputVideo: { duration: "9.999900", nb_frames: "300" }, + outputAudio: { duration: "10.005000" }, + }, + { durationSec: 10, targetFrames: 300 }, + ); + + expect(issues).toEqual([]); + }); + + it("rejects CUDA output that reports too few frames or a short video stream", () => { + const issues = validateNvidiaCudaExportSummary( + { + success: true, + targetFrames: 300, + durationSec: 10, + nativeSummary: { success: true, frames: 144 }, + outputVideo: { duration: "4.799952", nb_frames: "144" }, + outputAudio: { duration: "10.005000" }, + }, + { durationSec: 10, targetFrames: 300 }, + ); + + expect(issues).toEqual([ + "native frames 144 below expected minimum 285", + "output video frames 144 below expected minimum 285", + "output video duration 4.800s differs from expected 10.000s", + ]); + }); + + it("rejects audio CUDA output unless the helper reports timestamp-aligned frame selection", () => { + const issues = validateNvidiaCudaExportSummary( + { + success: true, + targetFrames: 300, + durationSec: 10, + nativeSummary: { + success: true, + frames: 300, + selectionStage: "decoder-policy-mapped-callback", + }, + outputVideo: { duration: "9.999900", nb_frames: "300" }, + outputAudio: { duration: "10.005000" }, + }, + { durationSec: 10, targetFrames: 300, requiresTimelineSync: true }, + ); + + expect(issues).toEqual([ + "CUDA timeline mode is not timestamp-aligned for audio export", + ]); + }); + + it("accepts audio CUDA output when the helper reports PTS-aligned selection", () => { + const issues = validateNvidiaCudaExportSummary( + { + success: true, + targetFrames: 300, + durationSec: 10, + nativeSummary: { + success: true, + frames: 300, + sourceTimestampMode: "pts", + selectionStage: "timestamp-mapped-callback", + }, + outputVideo: { duration: "9.999900", nb_frames: "300" }, + outputAudio: { duration: "10.005000" }, + }, + { durationSec: 10, targetFrames: 300, requiresTimelineSync: true }, + ); + + expect(issues).toEqual([]); + }); +}); + describe("parseWindowsGpuExportProgressLine", () => { it("parses bounded helper progress lines", () => { expect( diff --git a/electron/ipc/export/native-video.ts b/electron/ipc/export/native-video.ts index d7598f28..97964ada 100644 --- a/electron/ipc/export/native-video.ts +++ b/electron/ipc/export/native-video.ts @@ -14,6 +14,7 @@ import type { NativeStaticLayoutBackend, NativeStaticLayoutExportArgsConfig, NativeVideoAudioMuxMetrics, + NativeVideoExportAudioMode, NativeVideoExportFinishOptions, } from "../nativeVideoExport"; import { @@ -40,6 +41,7 @@ const getNowMs = () => performance.now(); const formatFfmpegSeconds = (milliseconds: number) => (milliseconds / 1000).toFixed(3); const MISSING_NATIVE_STATIC_BACKGROUND_COLOR = "#ffffff"; const NATIVE_EXPORT_HIGH_PRIORITY = os.constants.priority.PRIORITY_HIGH; +const NVIDIA_CUDA_ALLOW_AUDIO_EXPORT_ENV = "RECORDLY_NVIDIA_CUDA_ALLOW_AUDIO_EXPORT"; export type NativeVideoExportSession = { ffmpegProcess: ChildProcessByStdio; @@ -186,18 +188,24 @@ export interface NvidiaCudaExportSummary { bitrateMbps?: number; durationSec?: number; targetFrames?: number; + sourcePtsFrames?: number; + sourcePtsSource?: string; timingsMs?: { demux?: number; backgroundConvert?: number; cursorAtlas?: number; webcamConvert?: number; webcamDemux?: number; + sourcePtsProbe?: number; nativeEncode?: number; mux?: number; endToEnd?: number; }; nativeSummary?: { success?: boolean; + selectionStage?: string; + sourceTimestampMode?: string; + timelineMode?: string; frames?: number; totalMs?: number; fps?: number; @@ -332,6 +340,111 @@ export function parseNvidiaCudaExportSummary(stdout: string): NvidiaCudaExportSu } } +function getFiniteNumber(value: unknown) { + const numberValue = typeof value === "string" ? Number(value) : value; + return typeof numberValue === "number" && Number.isFinite(numberValue) + ? numberValue + : null; +} + +function getNvidiaCudaOutputStreamNumber(stream: unknown, property: string) { + if (!stream || typeof stream !== "object") { + return null; + } + + return getFiniteNumber((stream as Record)[property]); +} + +export function validateNvidiaCudaExportSummary( + summary: NvidiaCudaExportSummary, + expected: { + durationSec: number; + targetFrames: number; + requiresTimelineSync?: boolean; + }, +) { + const issues: string[] = []; + const expectedFrames = Math.max(1, Math.round(expected.targetFrames)); + const minimumFrames = Math.max(1, Math.floor(expectedFrames * 0.95)); + const expectedDurationSec = Math.max(0, expected.durationSec); + const durationToleranceSec = Math.min( + 2, + Math.max(0.5, expectedDurationSec * 0.02), + ); + const nativeFrames = getFiniteNumber(summary.nativeSummary?.frames); + const outputVideoFrames = getNvidiaCudaOutputStreamNumber( + summary.outputVideo, + "nb_frames", + ); + const outputVideoDurationSec = getNvidiaCudaOutputStreamNumber( + summary.outputVideo, + "duration", + ); + const outputAudioDurationSec = getNvidiaCudaOutputStreamNumber( + summary.outputAudio, + "duration", + ); + + if (!summary.outputVideo) { + issues.push("missing output video probe"); + } + if ( + expected.requiresTimelineSync && + !isNvidiaCudaTimestampAlignedSummary(summary) + ) { + issues.push( + "CUDA timeline mode is not timestamp-aligned for audio export", + ); + } + if (nativeFrames === null) { + issues.push("missing native frame count"); + } else if (nativeFrames < minimumFrames) { + issues.push( + `native frames ${nativeFrames} below expected minimum ${minimumFrames}`, + ); + } + if (outputVideoFrames !== null && outputVideoFrames < minimumFrames) { + issues.push( + `output video frames ${outputVideoFrames} below expected minimum ${minimumFrames}`, + ); + } + if ( + outputVideoDurationSec !== null && + Math.abs(outputVideoDurationSec - expectedDurationSec) > durationToleranceSec + ) { + issues.push( + `output video duration ${outputVideoDurationSec.toFixed( + 3, + )}s differs from expected ${expectedDurationSec.toFixed(3)}s`, + ); + } + if ( + outputAudioDurationSec !== null && + Math.abs(outputAudioDurationSec - expectedDurationSec) > durationToleranceSec + ) { + issues.push( + `output audio duration ${outputAudioDurationSec.toFixed( + 3, + )}s differs from expected ${expectedDurationSec.toFixed(3)}s`, + ); + } + + return issues; +} + +function isNvidiaCudaTimestampAlignedSummary(summary: NvidiaCudaExportSummary) { + const nativeSummary = summary.nativeSummary; + const timelineFields = [ + nativeSummary?.sourceTimestampMode, + nativeSummary?.timelineMode, + nativeSummary?.selectionStage, + ]; + return timelineFields.some((value) => { + const normalized = String(value ?? "").toLowerCase(); + return normalized.includes("pts") || normalized.includes("timestamp"); + }); +} + function shouldPersistNvidiaCudaExportDiagnostics() { return ( process.env.RECORDLY_NVIDIA_CUDA_EXPORT_DIAGNOSTICS === "1" || @@ -1048,12 +1161,34 @@ async function resolveExperimentalWindowsGpuExporterPath() { return null; } -function isExperimentalNvidiaCudaExportEnabled(options: NativeStaticLayoutExportOptions) { - return Boolean( - process.platform === "win32" && - process.env.RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT === "1" && - options.experimentalWindowsGpuCompositor, - ); +export function getNvidiaCudaAudioExportSkipReason( + audioMode: NativeVideoExportAudioMode | undefined, +) { + const resolvedAudioMode = audioMode ?? "none"; + if (resolvedAudioMode === "none") { + return null; + } + if (process.env[NVIDIA_CUDA_ALLOW_AUDIO_EXPORT_ENV] === "1") { + return null; + } + + return `audio-mode:${resolvedAudioMode}`; +} + +function getExperimentalNvidiaCudaExportSkipReason( + options: NativeStaticLayoutExportOptions, +) { + if (process.platform !== "win32") { + return "not-windows"; + } + if (process.env.RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT !== "1") { + return "env-disabled"; + } + if (!options.experimentalWindowsGpuCompositor) { + return "windows-gpu-compositor-disabled"; + } + + return getNvidiaCudaAudioExportSkipReason(options.audioOptions?.audioMode); } async function resolveExperimentalNvidiaCudaExportScriptPath() { @@ -1067,6 +1202,28 @@ async function resolveExperimentalNvidiaCudaExportScriptPath() { candidates.push(configuredPath); } candidates.push( + path.join( + process.cwd(), + "electron", + "native", + "nvidia-cuda-compositor", + "run-mp4-pipeline.mjs", + ), + path.join( + app.getAppPath(), + "electron", + "native", + "nvidia-cuda-compositor", + "run-mp4-pipeline.mjs", + ), + path.join( + process.resourcesPath, + "app.asar.unpacked", + "electron", + "native", + "nvidia-cuda-compositor", + "run-mp4-pipeline.mjs", + ), path.join(process.cwd(), ".tmp", "nvdec-nvenc-probe", "run-mp4-pipeline.mjs"), path.join(app.getAppPath(), ".tmp", "nvdec-nvenc-probe", "run-mp4-pipeline.mjs"), ); @@ -1664,6 +1821,7 @@ async function runExperimentalNvidiaCudaStaticLayoutExport( let stdout = ""; let stderr = ""; let stderrLineBuffer = ""; + let lastProgressPercentage = 0; let settled = false; const timeout = setTimeout(() => { if (settled) return; @@ -1693,8 +1851,13 @@ async function runExperimentalNvidiaCudaStaticLayoutExport( : elapsedMs > 0 && progress.currentFrame > 0 ? (progress.currentFrame * 1000) / elapsedMs : undefined; + lastProgressPercentage = Math.max( + lastProgressPercentage, + progress.percentage, + ); onProgress?.({ ...progress, + percentage: lastProgressPercentage, sessionId: options.sessionId, backend: "nvidia-cuda-compositor", elapsedMs, @@ -2030,7 +2193,23 @@ export async function exportNativeStaticLayoutVideo( } } let experimentalNvidiaCudaOptions = experimentalGpuOptions; - let shouldTryNvidiaCuda = isExperimentalNvidiaCudaExportEnabled(options); + const nvidiaCudaSkipReason = + getExperimentalNvidiaCudaExportSkipReason(options); + let shouldTryNvidiaCuda = nvidiaCudaSkipReason === null; + if ( + !shouldTryNvidiaCuda && + process.env.RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT === "1" && + nvidiaCudaSkipReason !== "env-disabled" + ) { + console.warn( + "[native-static-layout-export] Skipping NVIDIA CUDA compositor; falling back to Windows GPU compositor", + { + reason: nvidiaCudaSkipReason, + audioMode: options.audioOptions?.audioMode ?? "none", + overrideEnv: NVIDIA_CUDA_ALLOW_AUDIO_EXPORT_ENV, + }, + ); + } if (shouldTryNvidiaCuda && options.cursorTelemetry?.length) { const cursorTelemetryPath = await prepareNvidiaCudaCursorTelemetry( options, @@ -2064,6 +2243,21 @@ export async function exportNativeStaticLayoutVideo( session, onProgress, ); + const cudaValidationIssues = validateNvidiaCudaExportSummary( + cudaResult.summary, + { + durationSec: options.durationSec, + targetFrames: Math.ceil(options.durationSec * options.frameRate), + requiresTimelineSync: + (experimentalNvidiaCudaOptions.audioOptions?.audioMode ?? + "none") !== "none", + }, + ); + if (cudaValidationIssues.length > 0) { + throw new Error( + `Experimental NVIDIA CUDA compositor produced an invalid output: ${cudaValidationIssues.join("; ")}`, + ); + } console.info( "[native-static-layout-export] NVIDIA CUDA compositor completed", { @@ -2092,9 +2286,9 @@ export async function exportNativeStaticLayoutVideo( webcamOverlay: cudaResult.summary.nativeSummary?.webcamOverlay, cursorAtlas: cudaResult.summary.nativeSummary?.cursorAtlas, zoomOverlay: cudaResult.summary.nativeSummary?.zoomOverlay, - zoomSamples: cudaResult.summary.nativeSummary?.zoomSamples, - }, - ); + zoomSamples: cudaResult.summary.nativeSummary?.zoomSamples, + }, + ); const outputStat = await fs.stat(videoOnlyPath); metrics.chunkCount = 1; metrics.chunkDurationSec = options.durationSec; diff --git a/electron/ipc/register/recording.ts b/electron/ipc/register/recording.ts index 8ca4720f..a019a341 100644 --- a/electron/ipc/register/recording.ts +++ b/electron/ipc/register/recording.ts @@ -20,8 +20,8 @@ import { startNativeCursorMonitor, stopNativeCursorMonitor } from "../cursor/mon import { clamp, pauseCursorCapture, - resumeCursorCapture, resetCursorCaptureClock, + resumeCursorCapture, sampleCursorPoint, snapshotCursorTelemetryForPersistence, startCursorSampling, @@ -1082,6 +1082,18 @@ export function registerRecordingHandlers( try { return await finalizeStoredVideo(videoPath); } catch { + try { + await validateRecordedVideo(videoPath); + return { + success: false, + path: videoPath, + message: "Failed to mux native Windows recording", + error: String(error), + }; + } catch { + // The fallback path is not safely playable; surface the original mux error. + } + return { success: false, message: "Failed to mux native Windows recording", diff --git a/electron/native/bin/win32-x64/helpers-manifest.json b/electron/native/bin/win32-x64/helpers-manifest.json index 6b3d48aa..e2c5313f 100644 --- a/electron/native/bin/win32-x64/helpers-manifest.json +++ b/electron/native/bin/win32-x64/helpers-manifest.json @@ -23,6 +23,13 @@ "sourceDir": "electron/native/gpu-export-probe", "sourceFingerprint": "75bf080c4a5cbbcb1d42fb088a1545130c613e42d9e7681cc23f9151d1c8072b", "updatedAt": "2026-05-03T15:39:47.938Z" + }, + "recordly-nvidia-cuda-compositor": { + "binaryName": "recordly-nvidia-cuda-compositor.exe", + "binarySha256": "f274039c46445a3b8e57c9faa6307238d6facc04d5c88f7bb84c43563feda649", + "sourceDir": "electron/native/nvidia-cuda-compositor", + "sourceFingerprint": "bc6f657ff3226f8c3bcdc89354de4118d0ef0f3ac1b5fde6d496bda9b77b65c6", + "updatedAt": "2026-05-04T02:54:12.432Z" } } } diff --git a/electron/native/bin/win32-x64/recordly-nvidia-cuda-compositor.exe b/electron/native/bin/win32-x64/recordly-nvidia-cuda-compositor.exe new file mode 100644 index 0000000000000000000000000000000000000000..2dfd3e716d27a2d0dfee0bb4831a78ba15b1e7f0 GIT binary patch literal 451584 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�kCKP-hZ=w9iFod|X{n*R zsKRN*)H1|t&6X6vLnk%G6a%uBy2ls^G(Ke&_NI^mYU&47Ek zCp2bY=%Q?7aF*M!W0xu0wsIbXLxM^)kMljIyDC&za#d*j#RexF&-6wewzbTM1
Tp8k+^lyxC5BwnE5L1 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 O#I(t~_~-w^-~R?ZSJ-<1 literal 0 HcmV?d00001 diff --git a/electron/native/nvidia-cuda-compositor/CMakeLists.txt b/electron/native/nvidia-cuda-compositor/CMakeLists.txt new file mode 100644 index 00000000..e79d9630 --- /dev/null +++ b/electron/native/nvidia-cuda-compositor/CMakeLists.txt @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 3.24) + +project(recordly_nvidia_cuda_compositor LANGUAGES CXX CUDA) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CUDA_STANDARD 17) +set(CMAKE_CUDA_STANDARD_REQUIRED ON) + +set(RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT + "${CMAKE_CURRENT_LIST_DIR}/../../../.tmp/video-sdk-samples" + CACHE PATH + "Path to the NVIDIA Video Codec SDK samples checkout" +) +set(NVIDIA_SAMPLES_DIR "${RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT}/Samples") +set(NVCODEC_DIR "${NVIDIA_SAMPLES_DIR}/NvCodec") + +if(NOT EXISTS "${NVCODEC_DIR}") + message(FATAL_ERROR "NVIDIA Video Codec SDK NvCodec directory not found: ${NVCODEC_DIR}. Set RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT.") +endif() + +add_executable(recordly-nvidia-cuda-compositor + src/main.cu + "${NVCODEC_DIR}/NvDecoder/NvDecoder.cpp" + "${NVCODEC_DIR}/NvEncoder/NvEncoder.cpp" + "${NVCODEC_DIR}/NvEncoder/NvEncoderCuda.cpp" +) + +target_include_directories(recordly-nvidia-cuda-compositor PRIVATE + "${NVIDIA_SAMPLES_DIR}" + "${NVCODEC_DIR}" +) + +target_compile_definitions(recordly-nvidia-cuda-compositor PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN +) + +find_package(CUDAToolkit REQUIRED) +target_link_libraries(recordly-nvidia-cuda-compositor PRIVATE + CUDA::cuda_driver + "${NVCODEC_DIR}/Lib/x64/nvcuvid.lib" +) + +set_target_properties(recordly-nvidia-cuda-compositor PROPERTIES + CUDA_SEPARABLE_COMPILATION OFF +) diff --git a/electron/native/nvidia-cuda-compositor/render-tahoe-cursor-atlas.cjs b/electron/native/nvidia-cuda-compositor/render-tahoe-cursor-atlas.cjs new file mode 100644 index 00000000..233ed252 --- /dev/null +++ b/electron/native/nvidia-cuda-compositor/render-tahoe-cursor-atlas.cjs @@ -0,0 +1,144 @@ +const { app, BrowserWindow, ipcMain } = require("electron"); +const fs = require("node:fs"); +const path = require("node:path"); + +const drawHeight = 256; +const padding = 2; +const cursorTypes = [ + "arrow", + "text", + "pointer", + "crosshair", + "open-hand", + "closed-hand", + "resize-ew", + "resize-ns", + "not-allowed", +]; +const tahoeAssets = { + arrow: ["pointer-1__14-6.svg", 0.14, 0.06], + text: ["ibeam-1__50-44.svg", 0.5, 0.44], + pointer: ["pointinghand-1__40-10.svg", 0.4, 0.1], + crosshair: ["crosshair-1__50-50.svg", 0.5, 0.5], + "open-hand": ["openhand-1__55-57.svg", 0.55, 0.57], + "closed-hand": ["closedhand-1__50-46.svg", 0.5, 0.46], + "resize-ew": ["resizeeastwest-1__50-50.svg", 0.5, 0.5], + "resize-ns": ["resizenorthsouth-1__50-49.svg", 0.5, 0.49], + "not-allowed": ["notallowed-1__23-0.svg", 0.23, 0], +}; + +function arg(name, fallback = "") { + const index = process.argv.indexOf(name); + return index >= 0 && index + 1 < process.argv.length ? process.argv[index + 1] : fallback; +} + +const repoRoot = arg("--repo-root"); +const atlasRgbaPath = arg("--output-rgba"); +const atlasMetadataPath = arg("--output-metadata"); + +if (!repoRoot || !atlasRgbaPath || !atlasMetadataPath) { + console.error("Usage: electron render-tahoe-cursor-atlas.cjs --repo-root --output-rgba --output-metadata "); + process.exit(1); +} + +const assets = cursorTypes.map((type, index) => { + const [fileName, anchorX, anchorY] = tahoeAssets[type]; + return { + type, + index, + filePath: path.join(repoRoot, "src", "assets", "cursors", "tahoe", fileName), + anchorX, + anchorY, + }; +}); + +app.disableHardwareAcceleration(); + +app.whenReady().then(async () => { + const window = new BrowserWindow({ + show: false, + width: 1, + height: 1, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + backgroundThrottling: false, + }, + }); + + ipcMain.once("atlas-ready", (_event, result) => { + console.log(JSON.stringify(result)); + app.quit(); + }); + + const html = ` + + +`; + + await window.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(html)); +}); diff --git a/electron/native/nvidia-cuda-compositor/run-mp4-pipeline.mjs b/electron/native/nvidia-cuda-compositor/run-mp4-pipeline.mjs new file mode 100644 index 00000000..88f7bd4f --- /dev/null +++ b/electron/native/nvidia-cuda-compositor/run-mp4-pipeline.mjs @@ -0,0 +1,1104 @@ +import { spawn, spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import os from "node:os"; +import { basename, dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); +const repoRoot = resolve(scriptDir, "..", "..", ".."); + +const cursorTypes = [ + "arrow", + "text", + "pointer", + "crosshair", + "open-hand", + "closed-hand", + "resize-ew", + "resize-ns", + "not-allowed", +]; +const cursorTypeIndexes = new Map(cursorTypes.map((type, index) => [type, index])); + +function fail(message) { + throw new Error(message); +} + +function getArg(name, fallback = "") { + const index = process.argv.indexOf(name); + if (index === -1) { + return fallback; + } + if (index + 1 >= process.argv.length) { + fail(`Missing value for ${name}`); + } + return process.argv[index + 1]; +} + +function getNumberArg(name, fallback) { + const value = getArg(name, ""); + if (!value) { + return fallback; + } + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed <= 0) { + fail(`Invalid ${name}: ${value}`); + } + return parsed; +} + +function getNonNegativeNumberArg(name, fallback) { + const value = getArg(name, ""); + if (!value) { + return fallback; + } + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed < 0) { + fail(`Invalid ${name}: ${value}`); + } + return parsed; +} + +function hasArg(name) { + return process.argv.includes(name); +} + +function shouldRaiseChildPriority() { + return process.env.RECORDLY_NVIDIA_CUDA_EXPORT_HIGH_PRIORITY !== "0"; +} + +function raiseChildPriority(child, label) { + if (!shouldRaiseChildPriority() || !child.pid) { + return false; + } + + try { + os.setPriority(child.pid, os.constants.priority.PRIORITY_HIGH); + return true; + } catch (error) { + console.warn(`[nvidia-cuda-export] Failed to raise ${label} priority: ${error}`); + return false; + } +} + +function run(command, args, options = {}) { + const startedAt = performance.now(); + const result = spawnSync(command, args, { + encoding: "utf8", + maxBuffer: 64 * 1024 * 1024, + windowsHide: true, + ...options, + }); + const elapsedMs = performance.now() - startedAt; + if (result.error) { + fail(`${command} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + const stderr = result.stderr?.trim(); + const stdout = result.stdout?.trim(); + fail( + `${command} exited with ${result.status}` + + (stderr ? `\nSTDERR:\n${stderr}` : "") + + (stdout ? `\nSTDOUT:\n${stdout}` : ""), + ); + } + return { + elapsedMs, + stdout: result.stdout ?? "", + stderr: result.stderr ?? "", + }; +} + +function emitPreparationProgress(totalFrames, percentage) { + const payload = { + currentFrame: 0, + totalFrames: Math.max(1, Math.floor(totalFrames)), + percentage: Number(Math.min(99, Math.max(0, percentage)).toFixed(2)), + }; + process.stderr.write(`PROGRESS ${JSON.stringify(payload)}\n`); +} + +function parseFfmpegStatsFrameCount(stderr) { + const matches = [...String(stderr ?? "").matchAll(/frame=\s*(\d+)/g)]; + if (!matches.length) { + return 0; + } + const frameCount = Number(matches[matches.length - 1][1]); + return Number.isFinite(frameCount) && frameCount > 0 ? Math.floor(frameCount) : 0; +} + +function sampleGpu() { + const result = spawnSync( + "nvidia-smi", + [ + "--query-gpu=timestamp,temperature.gpu,power.draw,pstate,utilization.gpu,utilization.decoder,utilization.encoder,clocks.sm,clocks.mem", + "--format=csv,noheader,nounits", + ], + { encoding: "utf8", windowsHide: true }, + ); + if (result.status !== 0 || !result.stdout.trim()) { + return null; + } + const parts = result.stdout.trim().split(",").map((part) => part.trim()); + return { + timestamp: parts[0], + temperatureC: Number(parts[1]), + powerW: Number(parts[2]), + pstate: parts[3], + gpuUtilizationPct: Number(parts[4]), + decoderUtilizationPct: Number(parts[5]), + encoderUtilizationPct: Number(parts[6]), + smClockMhz: Number(parts[7]), + memoryClockMhz: Number(parts[8]), + }; +} + +function summarizeGpuSamples(samples) { + if (!samples.length) { + return null; + } + const numeric = [ + "temperatureC", + "powerW", + "gpuUtilizationPct", + "decoderUtilizationPct", + "encoderUtilizationPct", + "smClockMhz", + "memoryClockMhz", + ]; + const summary = { + samples: samples.length, + pstateValues: [...new Set(samples.map((sample) => sample.pstate))], + }; + for (const key of numeric) { + const values = samples.map((sample) => sample[key]).filter(Number.isFinite); + if (!values.length) { + continue; + } + summary[key] = { + min: Math.min(...values), + max: Math.max(...values), + avg: Number((values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(2)), + }; + } + return summary; +} + +function cursorBounceScale(interactionType, ageMs, durationMs = 180) { + if (!["click", "double-click", "right-click", "middle-click"].includes(interactionType)) { + return 1; + } + if (ageMs < 0 || ageMs > durationMs) { + return 1; + } + const progress = 1 - ageMs / durationMs; + return Math.max(0.72, 1 - Math.sin(progress * Math.PI) * 0.08); +} + +function latestClickSample(samples, sampleIndex) { + for (let index = sampleIndex; index >= 0; index -= 1) { + const sample = samples[index]; + if (["click", "double-click", "right-click", "middle-click"].includes(sample?.interactionType)) { + return sample; + } + } + return null; +} + +function writeCursorSamples(cursorPayload, outputPath) { + const samples = Array.isArray(cursorPayload.samples) ? cursorPayload.samples : []; + const cursorLines = samples + .map((sample, index) => { + if ( + !Number.isFinite(sample?.timeMs) || + !Number.isFinite(sample?.cx) || + !Number.isFinite(sample?.cy) + ) { + return null; + } + const clickSample = latestClickSample(samples, index); + const bounceScale = Number.isFinite(sample.bounceScale) + ? sample.bounceScale + : clickSample + ? cursorBounceScale(clickSample.interactionType, sample.timeMs - clickSample.timeMs) + : 1; + return [ + sample.timeMs, + sample.cx, + sample.cy, + cursorTypeIndexes.get(sample.cursorType) ?? + (Number.isFinite(sample.cursorTypeIndex) ? Math.max(0, Math.min(8, Math.round(sample.cursorTypeIndex))) : 0), + Number(bounceScale.toFixed(4)), + ].join("\t"); + }) + .filter(Boolean) + .join("\n"); + writeFileSync(outputPath, cursorLines ? `${cursorLines}\n` : ""); + return samples.length; +} + +function renderTahoeCursorAtlas(workDir) { + const rgbaPath = join(workDir, "tahoe-cursor-atlas.rgba"); + const metadataPath = join(workDir, "tahoe-cursor-atlas.tsv"); + const electronPath = require("electron"); + const render = run(electronPath, [ + join(scriptDir, "render-tahoe-cursor-atlas.cjs"), + "--repo-root", + repoRoot, + "--output-rgba", + rgbaPath, + "--output-metadata", + metadataPath, + ], { + env: { + ...process.env, + ELECTRON_DISABLE_SECURITY_WARNINGS: "1", + }, + }); + const resultLine = render.stdout.trim().split(/\r?\n/).filter(Boolean).pop(); + if (!resultLine) { + fail("Cursor atlas renderer did not report a result"); + } + const result = JSON.parse(resultLine); + if (result.error) { + fail(`Cursor atlas renderer failed: ${result.error}`); + } + return { + rgbaPath, + metadataPath, + width: result.width, + height: result.height, + entries: result.entries, + elapsedMs: render.elapsedMs, + }; +} + +function prepareExternalCursorAtlas(workDir, pngPath, metadataPath) { + const startedAt = performance.now(); + const resolvedPngPath = resolve(pngPath); + const resolvedMetadataPath = resolve(metadataPath); + if (!existsSync(resolvedPngPath)) { + fail(`Cursor atlas PNG does not exist: ${resolvedPngPath}`); + } + if (!existsSync(resolvedMetadataPath)) { + fail(`Cursor atlas metadata does not exist: ${resolvedMetadataPath}`); + } + + const json = ffprobeJson([ + "-select_streams", + "v:0", + "-show_entries", + "stream=width,height", + resolvedPngPath, + ]); + const stream = json.streams?.[0]; + const width = Number(stream?.width); + const height = Number(stream?.height); + if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { + fail(`Invalid cursor atlas dimensions: ${resolvedPngPath}`); + } + + const rgbaPath = join(workDir, "external-cursor-atlas.rgba"); + run("ffmpeg", [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-i", + resolvedPngPath, + "-f", + "rawvideo", + "-pix_fmt", + "rgba", + rgbaPath, + ]); + const entries = readFileSync(resolvedMetadataPath, "utf8") + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean).length; + if (entries === 0) { + fail(`Cursor atlas metadata is empty: ${resolvedMetadataPath}`); + } + + return { + rgbaPath, + metadataPath: resolvedMetadataPath, + width, + height, + entries, + elapsedMs: performance.now() - startedAt, + }; +} + +async function runWithGpuMonitor(command, args, sampleIntervalMs) { + const samples = []; + const startedAt = performance.now(); + const shouldSampleGpu = Number.isFinite(sampleIntervalMs) && sampleIntervalMs > 0; + let stdout = ""; + let stderr = ""; + + const child = spawn(command, args, { + windowsHide: true, + stdio: ["ignore", "pipe", "pipe"], + }); + const priorityBoosted = raiseChildPriority(child, "native CUDA encoder"); + + const collectSample = () => { + if (!shouldSampleGpu) { + return; + } + const sample = sampleGpu(); + if (sample) { + samples.push({ + elapsedMs: Number((performance.now() - startedAt).toFixed(2)), + ...sample, + }); + } + }; + collectSample(); + const interval = shouldSampleGpu ? setInterval(collectSample, sampleIntervalMs) : null; + + child.stdout.on("data", (chunk) => { + stdout += chunk.toString(); + }); + child.stderr.on("data", (chunk) => { + const text = chunk.toString(); + stderr += text; + process.stderr.write(text); + }); + + const status = await new Promise((resolve, reject) => { + child.on("error", reject); + child.on("close", resolve); + }); + if (interval) { + clearInterval(interval); + } + collectSample(); + + const elapsedMs = performance.now() - startedAt; + if (status !== 0) { + fail( + `${command} exited with ${status}` + + (stderr.trim() ? `\nSTDERR:\n${stderr.trim()}` : "") + + (stdout.trim() ? `\nSTDOUT:\n${stdout.trim()}` : ""), + ); + } + return { + elapsedMs, + stdout, + stderr, + gpuSamples: samples, + gpuSummary: summarizeGpuSamples(samples), + priorityBoosted, + }; +} + +function ffprobeJson(args) { + const result = run("ffprobe", ["-v", "error", ...args, "-of", "json"]); + return JSON.parse(result.stdout); +} + +function ffprobeCsv(args) { + const result = run("ffprobe", ["-v", "error", ...args, "-of", "csv=p=0"]); + return result.stdout; +} + +function getVideoInfo(inputPath) { + const json = ffprobeJson([ + "-select_streams", + "v:0", + "-show_entries", + "stream=codec_name,width,height,duration,avg_frame_rate,nb_frames", + inputPath, + ]); + let stream = json.streams?.[0]; + if (!stream) { + fail(`No video stream found in ${inputPath}`); + } + if (stream.codec_name !== "h264") { + fail(`The NVIDIA CUDA compositor currently expects H.264 input, got ${stream.codec_name}`); + } + const durationSec = Number(stream.duration); + if (!Number.isFinite(durationSec) || durationSec <= 0) { + fail("ffprobe did not return a valid video duration"); + } + let sourceFrames = Number(stream.nb_frames); + if (!Number.isFinite(sourceFrames) || sourceFrames <= 0) { + const countedJson = ffprobeJson([ + "-count_frames", + "-select_streams", + "v:0", + "-show_entries", + "stream=codec_name,width,height,duration,avg_frame_rate,nb_frames,nb_read_frames", + inputPath, + ]); + stream = countedJson.streams?.[0] ?? stream; + sourceFrames = Number(stream.nb_read_frames || stream.nb_frames); + } + if (!Number.isFinite(sourceFrames) || sourceFrames <= 0) { + fail("ffprobe did not return a valid source frame count"); + } + return { + codec: stream.codec_name, + width: Number(stream.width), + height: Number(stream.height), + durationSec, + sourceFrames, + avgFrameRate: stream.avg_frame_rate, + }; +} + +function normalizeMonotonicTimestamps(timestamps) { + if (timestamps.length < 2) { + return []; + } + + const first = timestamps[0]; + const normalized = timestamps + .map((value) => Math.max(0, value - first)) + .filter((value, index, values) => index === 0 || value >= values[index - 1]); + return normalized.length === timestamps.length ? normalized : []; +} + +function parseTimestampCsv(csv) { + return csv + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => { + const columns = line.split(","); + const value = columns + .map((column) => Number(column.trim())) + .find((candidate) => Number.isFinite(candidate)); + return value ?? Number.NaN; + }) + .filter((value) => Number.isFinite(value)); +} + +function getVideoPacketPts(inputPath, durationSec) { + const csv = ffprobeCsv([ + "-select_streams", + "v:0", + "-read_intervals", + `%+${durationSec}`, + "-show_packets", + "-show_entries", + "packet=pts_time,dts_time", + inputPath, + ]); + return normalizeMonotonicTimestamps(parseTimestampCsv(csv)); +} + +function getVideoFramePts(inputPath, durationSec) { + const csv = ffprobeCsv([ + "-select_streams", + "v:0", + "-read_intervals", + `%+${durationSec}`, + "-show_frames", + "-show_entries", + "frame=best_effort_timestamp_time", + inputPath, + ]); + return normalizeMonotonicTimestamps(parseTimestampCsv(csv)); +} + +function writeFramePtsSidecar(inputPath, durationSec, outputPath) { + const startedAt = performance.now(); + let source = "packet-pts"; + let timestamps = getVideoPacketPts(inputPath, durationSec); + if (timestamps.length === 0) { + source = "frame-pts"; + timestamps = getVideoFramePts(inputPath, durationSec); + } + const elapsedMs = performance.now() - startedAt; + if (timestamps.length === 0) { + return { path: null, frames: 0, elapsedMs, source: "none" }; + } + + writeFileSync(outputPath, timestamps.map((value) => value.toFixed(9)).join("\n")); + return { path: outputPath, frames: timestamps.length, elapsedMs, source }; +} + +function roundedRectMaskExpression({ x, y, width, height, radius }) { + const right = x + width; + const bottom = y + height; + const cornerRight = right - radius; + const cornerBottom = bottom - radius; + const radiusSquared = radius * radius; + const centerBand = `between(X,${x + radius},${cornerRight})*between(Y,${y},${bottom})`; + const middleBand = `between(X,${x},${right})*between(Y,${y + radius},${cornerBottom})`; + const topLeft = `lte((X-${x + radius})*(X-${x + radius})+(Y-${y + radius})*(Y-${y + radius}),${radiusSquared})*lte(X,${x + radius})*lte(Y,${y + radius})`; + const topRight = `lte((X-${cornerRight})*(X-${cornerRight})+(Y-${y + radius})*(Y-${y + radius}),${radiusSquared})*gte(X,${cornerRight})*lte(Y,${y + radius})`; + const bottomLeft = `lte((X-${x + radius})*(X-${x + radius})+(Y-${cornerBottom})*(Y-${cornerBottom}),${radiusSquared})*lte(X,${x + radius})*gte(Y,${cornerBottom})`; + const bottomRight = `lte((X-${cornerRight})*(X-${cornerRight})+(Y-${cornerBottom})*(Y-${cornerBottom}),${radiusSquared})*gte(X,${cornerRight})*gte(Y,${cornerBottom})`; + return `${centerBand}+${middleBand}+${topLeft}+${topRight}+${bottomLeft}+${bottomRight}`; +} + +function createBackgroundFilter(videoInfo, shadowOptions) { + const base = `[0:v]scale=${videoInfo.width}:${videoInfo.height}:force_original_aspect_ratio=increase,crop=${videoInfo.width}:${videoInfo.height},format=rgba[bg]`; + if (!shadowOptions) { + return { + filterArgs: [ + "-vf", + `scale=${videoInfo.width}:${videoInfo.height}:force_original_aspect_ratio=increase,crop=${videoInfo.width}:${videoInfo.height},format=nv12`, + ], + bakedShadow: false, + }; + } + + const shadowAlpha = Math.round(255 * Math.min(0.5, shadowOptions.intensityPct / 200)); + const shadowBlur = Math.max(12, Math.round(shadowOptions.radius * 1.5)); + const mask = roundedRectMaskExpression({ + x: shadowOptions.x, + y: shadowOptions.y, + width: shadowOptions.width, + height: shadowOptions.height, + radius: shadowOptions.radius, + }); + const shadow = `[1:v]format=rgba,geq=r='0':g='0':b='0':a='if(${mask},${shadowAlpha},0)',boxblur=luma_radius=${shadowBlur}:luma_power=1:chroma_radius=${shadowBlur}:chroma_power=1:alpha_radius=${shadowBlur}:alpha_power=1[shadow]`; + return { + filterArgs: [ + "-f", + "lavfi", + "-i", + `color=c=black@0.0:s=${videoInfo.width}x${videoInfo.height}:d=1`, + "-filter_complex", + `${base};${shadow};[bg][shadow]overlay=format=auto,format=nv12[out]`, + "-map", + "[out]", + ], + bakedShadow: true, + shadowAlpha, + shadowBlur, + }; +} + +function parseProbeSummary(stdout) { + const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); + const jsonLine = lines.find((line) => line.startsWith("{") && line.includes("\"success\"")); + if (!jsonLine) { + fail(`Native probe did not emit a JSON summary:\n${stdout}`); + } + try { + return JSON.parse(jsonLine); + } catch { + // The helper currently prints raw Windows paths with backslashes. + // Keep parsing resilient while this remains a throwaway benchmark tool. + return JSON.parse(jsonLine.replace(/,"outputPath":".*"\}$/, "}")); + } +} + +const inputPath = resolve(getArg("--input")); +const outputPath = resolve(getArg("--output", join(scriptDir, "recordly-nvdec-nvenc-mp4-output.mp4"))); +const fps = Math.round(getNumberArg("--fps", 30)); +const bitrateMbps = Math.round(getNumberArg("--bitrate-mbps", 18)); +const workDir = resolve(getArg("--work-dir", join(scriptDir, "mp4-work"))); +const reuseIntermediates = hasArg("--reuse-intermediates"); +const reuseDemux = hasArg("--reuse-demux") || reuseIntermediates; +const sampleGpuDuringEncode = hasArg("--sample-gpu"); +const gpuSampleIntervalMs = Math.round(getNumberArg("--gpu-sample-interval-ms", 1000)); +const streamSync = hasArg("--stream-sync"); +const prewarmMs = Math.round(getNumberArg("--prewarm-ms", 0)); +const maxOutputFrames = Math.round(getNumberArg("--max-output-frames", 0)); +const requestedDurationSec = getNumberArg("--duration-sec", 0); +const chunkMb = Math.round(getNumberArg("--chunk-mb", 4)); +const skipMux = hasArg("--skip-mux"); +const videoOnly = hasArg("--video-only"); +const contentX = Math.round(getNonNegativeNumberArg("--content-x", 0)); +const contentY = Math.round(getNonNegativeNumberArg("--content-y", 0)); +const contentWidth = Math.round(getNumberArg("--content-width", 0)); +const contentHeight = Math.round(getNumberArg("--content-height", 0)); +const radius = Math.round(getNonNegativeNumberArg("--radius", 0)); +const backgroundY = Math.round(getNonNegativeNumberArg("--background-y", 16)); +const backgroundU = Math.round(getNonNegativeNumberArg("--background-u", 128)); +const backgroundV = Math.round(getNonNegativeNumberArg("--background-v", 128)); +const backgroundImage = getArg("--background-image", ""); +const backgroundNv12 = getArg("--background-nv12", ""); +const shadowOffsetY = Math.round(getNonNegativeNumberArg("--shadow-offset-y", 0)); +const shadowIntensityPct = Math.round(getNonNegativeNumberArg("--shadow-intensity-pct", 0)); +const webcamInput = getArg("--webcam-input", ""); +const webcamX = Math.round(getNonNegativeNumberArg("--webcam-x", 0)); +const webcamY = Math.round(getNonNegativeNumberArg("--webcam-y", 0)); +const webcamSize = Math.round(getNumberArg("--webcam-size", 0)); +const webcamRadius = Math.round(getNonNegativeNumberArg("--webcam-radius", 0)); +const webcamMirror = hasArg("--webcam-mirror"); +const webcamStream = hasArg("--webcam-stream"); +const cursorJson = getArg("--cursor-json", ""); +const cursorHeight = Math.round(getNumberArg("--cursor-height", 0)); +const cursorStyle = getArg("--cursor-style", "vector"); +const cursorAtlasPng = getArg("--cursor-atlas-png", ""); +const cursorAtlasMetadata = getArg("--cursor-atlas-metadata", ""); +const zoomTelemetry = getArg("--zoom-telemetry", ""); + +if (!existsSync(inputPath)) { + fail(`Input does not exist: ${inputPath}`); +} +mkdirSync(workDir, { recursive: true }); +mkdirSync(dirname(outputPath), { recursive: true }); + +function resolveNativeProbePath() { + const configuredPath = process.env.RECORDLY_NVIDIA_CUDA_EXPORT_EXE; + const platformArch = process.arch === "arm64" ? "win32-arm64" : "win32-x64"; + const candidates = [ + configuredPath, + join(scriptDir, "build", "Release", "recordly-nvidia-cuda-compositor.exe"), + join(repoRoot, "electron", "native", "bin", platformArch, "recordly-nvidia-cuda-compositor.exe"), + // Backward-compatible legacy helper path while old work dirs are being retired. + join(scriptDir, "build", "Release", "recordly-nvdec-nvenc-probe.exe"), + ].filter(Boolean); + + for (const candidate of candidates) { + if (existsSync(candidate)) { + return candidate; + } + } + + fail(`Native NVIDIA CUDA compositor is not built. Checked: ${candidates.join(", ")}`); +} +const nativeProbe = resolveNativeProbePath(); + +const baseName = basename(inputPath).replace(/\.[^.]+$/, ""); +const webcamBaseName = webcamInput + ? basename(webcamInput).replace(/\.[^.]+$/, "") + : `${baseName}.webcam`; +const annexBPath = join(workDir, `${baseName}.annexb.h264`); +const webcamAnnexBPath = join(workDir, `${webcamBaseName}.annexb.h264`); +const cursorSamplesPath = join(workDir, `${baseName}.cursor.tsv`); +const encodedPath = join(workDir, `${baseName}.mapped-callback.h264`); +const shouldBakeStaticShadow = + Boolean(backgroundImage) && + contentWidth > 0 && + contentHeight > 0 && + shadowOffsetY > 0 && + shadowIntensityPct > 0; +const backgroundSuffix = shouldBakeStaticShadow + ? `.shadow-${shadowOffsetY}-${shadowIntensityPct}` + : ""; +const generatedBackgroundNv12Path = join( + workDir, + `${baseName}${backgroundSuffix}.background.nv12`, +); +const generatedWebcamNv12Path = join( + workDir, + `${baseName}.webcam-${webcamSize}${webcamMirror ? "-mirror" : ""}.nv12`, +); +const sourcePtsPath = join(workDir, `${baseName}.source-pts.csv`); + +const videoInfo = getVideoInfo(inputPath); +const webcamInfo = webcamInput ? getVideoInfo(webcamInput) : null; +const durationSec = + requestedDurationSec > 0 ? Math.min(videoInfo.durationSec, requestedDurationSec) : videoInfo.durationSec; +const targetFrames = Math.ceil(durationSec * fps); +emitPreparationProgress(targetFrames, 1); +let sourceWindowFrames = Math.max( + 1, + Math.min(videoInfo.sourceFrames, Math.ceil((videoInfo.sourceFrames * durationSec) / videoInfo.durationSec)), +); +let webcamSourceWindowFrames = webcamInfo + ? Math.max( + 1, + Math.min(webcamInfo.sourceFrames, Math.ceil((webcamInfo.sourceFrames * durationSec) / webcamInfo.durationSec)), + ) + : 0; +const backgroundNv12Path = backgroundImage + ? generatedBackgroundNv12Path + : backgroundNv12; +const backgroundFilter = createBackgroundFilter( + videoInfo, + shouldBakeStaticShadow + ? { + x: contentX, + y: contentY + shadowOffsetY, + width: contentWidth, + height: contentHeight, + radius: radius + 8, + intensityPct: shadowIntensityPct, + } + : null, +); + +const backgroundConvert = + backgroundImage && !(reuseIntermediates && existsSync(backgroundNv12Path)) + ? run("ffmpeg", [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-i", + resolve(backgroundImage), + ...backgroundFilter.filterArgs, + "-frames:v", + "1", + "-f", + "rawvideo", + backgroundNv12Path, + ]) + : { elapsedMs: 0 }; + +const webcamConvert = + webcamInput && !webcamStream && webcamSize > 0 && !(reuseIntermediates && existsSync(generatedWebcamNv12Path)) + ? run("ffmpeg", [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-i", + resolve(webcamInput), + "-vf", + `${webcamMirror ? "hflip," : ""}scale=${webcamSize}:${webcamSize}:force_original_aspect_ratio=increase,crop=${webcamSize}:${webcamSize},format=nv12`, + "-frames:v", + "1", + "-f", + "rawvideo", + generatedWebcamNv12Path, + ]) + : { elapsedMs: 0 }; + +const webcamDemux = + webcamInput && webcamStream && !(reuseDemux && existsSync(webcamAnnexBPath)) + ? run("ffmpeg", [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-stats", + "-i", + resolve(webcamInput), + "-t", + String(durationSec), + "-map", + "0:v:0", + "-c:v", + "copy", + "-bsf:v", + "h264_mp4toannexb", + "-an", + webcamAnnexBPath, + ]) + : { elapsedMs: 0 }; + +if (cursorJson) { + const cursorPayload = JSON.parse(readFileSync(resolve(cursorJson), "utf8")); + writeCursorSamples(cursorPayload, cursorSamplesPath); +} +const cursorAtlas = + cursorJson && cursorHeight > 0 && cursorAtlasPng && cursorAtlasMetadata + ? prepareExternalCursorAtlas(workDir, cursorAtlasPng, cursorAtlasMetadata) + : cursorJson && cursorHeight > 0 && cursorStyle === "tahoe" + ? renderTahoeCursorAtlas(workDir) + : null; + +const demux = reuseDemux && existsSync(annexBPath) + ? { elapsedMs: 0 } + : run("ffmpeg", [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-stats", + "-i", + inputPath, + "-t", + String(durationSec), + "-map", + "0:v:0", + "-c:v", + "copy", + "-bsf:v", + "h264_mp4toannexb", + "-an", + annexBPath, + ]); +const demuxFrameCount = parseFfmpegStatsFrameCount(demux.stderr); +if (demuxFrameCount > 0) { + sourceWindowFrames = demuxFrameCount; +} +const webcamDemuxFrameCount = parseFfmpegStatsFrameCount(webcamDemux.stderr); +if (webcamDemuxFrameCount > 0) { + webcamSourceWindowFrames = webcamDemuxFrameCount; +} +emitPreparationProgress(targetFrames, 2); +const sourcePts = writeFramePtsSidecar(inputPath, durationSec, sourcePtsPath); +emitPreparationProgress(targetFrames, 3); + +const encodeArgs = [ + "--input", + annexBPath, + "--output", + encodedPath, + "--fps", + String(fps), + "--input-frames", + String(sourceWindowFrames), + "--target-frames", + String(targetFrames), + "--bitrate-mbps", + String(bitrateMbps), + "--callback-encode", + "--chunk-mb", + String(chunkMb), +]; +if (sourcePts.path && sourcePts.frames >= sourceWindowFrames) { + encodeArgs.push("--source-pts", sourcePts.path); +} +if (maxOutputFrames > 0) { + encodeArgs.push("--max-frames", String(maxOutputFrames)); +} +if (cursorJson && cursorHeight > 0) { + encodeArgs.push("--cursor-samples", cursorSamplesPath, "--cursor-height", String(cursorHeight)); + if (cursorAtlas) { + encodeArgs.push( + "--cursor-atlas-rgba", + cursorAtlas.rgbaPath, + "--cursor-atlas-metadata", + cursorAtlas.metadataPath, + "--cursor-atlas-width", + String(cursorAtlas.width), + "--cursor-atlas-height", + String(cursorAtlas.height), + ); + } +} +if (streamSync) { + encodeArgs.push("--stream-sync"); +} +if (prewarmMs > 0) { + encodeArgs.push("--prewarm-ms", String(prewarmMs)); +} +if (contentWidth > 0 && contentHeight > 0) { + encodeArgs.push( + "--content-x", + String(contentX), + "--content-y", + String(contentY), + "--content-width", + String(contentWidth), + "--content-height", + String(contentHeight), + "--radius", + String(radius), + "--background-y", + String(backgroundY), + "--background-u", + String(backgroundU), + "--background-v", + String(backgroundV), + ); + if (backgroundNv12Path) { + encodeArgs.push("--background-nv12", backgroundNv12Path); + } + if (!shouldBakeStaticShadow && shadowOffsetY > 0 && shadowIntensityPct > 0) { + encodeArgs.push( + "--shadow-offset-y", + String(shadowOffsetY), + "--shadow-intensity-pct", + String(shadowIntensityPct), + ); + } + if (webcamInput && webcamSize > 0) { + encodeArgs.push( + "--webcam-x", + String(webcamX), + "--webcam-y", + String(webcamY), + "--webcam-size", + String(webcamSize), + "--webcam-radius", + String(webcamRadius), + ); + if (webcamMirror) { + encodeArgs.push("--webcam-mirror"); + } + if (webcamStream) { + encodeArgs.push( + "--webcam-annexb", + webcamAnnexBPath, + "--webcam-input-frames", + String(webcamSourceWindowFrames), + "--webcam-target-frames", + String(targetFrames), + "--webcam-source-width", + String(webcamInfo.width), + "--webcam-source-height", + String(webcamInfo.height), + ); + } else { + encodeArgs.push("--webcam-nv12", generatedWebcamNv12Path); + } + } +} +if (zoomTelemetry) { + encodeArgs.push("--zoom-samples", resolve(zoomTelemetry)); +} +const encode = reuseIntermediates && existsSync(encodedPath) + ? { elapsedMs: 0, stdout: "", gpuSummary: null } + : await runWithGpuMonitor( + nativeProbe, + encodeArgs, + sampleGpuDuringEncode ? gpuSampleIntervalMs : 0, + ); +const nativeSummary = encode.stdout ? parseProbeSummary(encode.stdout) : null; + +const mux = skipMux + ? { elapsedMs: 0 } + : videoOnly + ? run("ffmpeg", [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-framerate", + String(fps), + "-i", + encodedPath, + "-map", + "0:v:0", + "-c:v", + "copy", + outputPath, + ]) + : run("ffmpeg", [ + "-y", + "-hide_banner", + "-loglevel", + "error", + "-framerate", + String(fps), + "-i", + encodedPath, + "-i", + inputPath, + "-map", + "0:v:0", + "-map", + "1:a?", + "-c:v", + "copy", + "-c:a", + "copy", + "-t", + String(durationSec), + outputPath, +]); + +const outputInfo = skipMux + ? { streams: [] } + : ffprobeJson([ + "-show_entries", + "stream=index,codec_type,codec_name,width,height,duration,avg_frame_rate,nb_frames", + outputPath, + ]); +const outputStreams = outputInfo.streams ?? []; +const outputVideo = outputStreams.find((stream) => stream.codec_type === "video") ?? null; +const outputAudio = outputStreams.find((stream) => stream.codec_type === "audio") ?? null; + +console.log( + JSON.stringify( + { + success: true, + inputPath, + outputPath, + fps, + bitrateMbps, + streamSync, + prewarmMs, + maxOutputFrames, + chunkMb, + skipMux, + videoOnly, + durationSec, + staticLayout: + contentWidth > 0 && contentHeight > 0 + ? { + contentX, + contentY, + contentWidth, + contentHeight, + radius, + backgroundY, + backgroundU, + backgroundV, + shadowOffsetY, + shadowIntensityPct, + shadowBakedIntoBackground: shouldBakeStaticShadow, + backgroundShadowAlpha: backgroundFilter.shadowAlpha ?? null, + backgroundShadowBlur: backgroundFilter.shadowBlur ?? null, + webcam: + webcamInput && webcamSize > 0 + ? { + inputPath: resolve(webcamInput), + x: webcamX, + y: webcamY, + size: webcamSize, + radius: webcamRadius, + mirror: webcamMirror, + staticFrameOnly: !webcamStream, + stream: webcamStream, + } + : null, + cursor: + cursorJson && cursorHeight > 0 + ? { + inputPath: resolve(cursorJson), + height: cursorHeight, + style: cursorStyle, + atlas: cursorAtlas + ? { + width: cursorAtlas.width, + height: cursorAtlas.height, + entries: cursorAtlas.entries, + } + : null, + } + : null, + zoom: zoomTelemetry + ? { + inputPath: resolve(zoomTelemetry), + } + : null, + } + : null, + gpuSampleIntervalMs: sampleGpuDuringEncode ? gpuSampleIntervalMs : null, + videoInfo, + sourceWindowFrames, + sourcePtsFrames: sourcePts.frames, + sourcePtsSource: sourcePts.source, + targetFrames, + timingsMs: { + demux: Number(demux.elapsedMs.toFixed(2)), + backgroundConvert: Number(backgroundConvert.elapsedMs.toFixed(2)), + cursorAtlas: Number((cursorAtlas?.elapsedMs ?? 0).toFixed(2)), + webcamConvert: Number(webcamConvert.elapsedMs.toFixed(2)), + webcamDemux: Number(webcamDemux.elapsedMs.toFixed(2)), + sourcePtsProbe: Number(sourcePts.elapsedMs.toFixed(2)), + nativeEncode: Number(encode.elapsedMs.toFixed(2)), + mux: Number(mux.elapsedMs.toFixed(2)), + endToEnd: Number( + ( + backgroundConvert.elapsedMs + + (cursorAtlas?.elapsedMs ?? 0) + + webcamConvert.elapsedMs + + webcamDemux.elapsedMs + + demux.elapsedMs + + sourcePts.elapsedMs + + encode.elapsedMs + + mux.elapsedMs + ).toFixed(2), + ), + }, + nativeSummary, + nativeProcessPriorityBoosted: encode.priorityBoosted ?? false, + gpuSamples: encode.gpuSamples ?? [], + gpuSummary: encode.gpuSummary ?? null, + outputVideo, + outputAudio, + outputStreams, + }, + null, + 2, + ), +); diff --git a/electron/native/nvidia-cuda-compositor/src/main.cu b/electron/native/nvidia-cuda-compositor/src/main.cu new file mode 100644 index 00000000..385dc18b --- /dev/null +++ b/electron/native/nvidia-cuda-compositor/src/main.cu @@ -0,0 +1,3006 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "NvDecoder/NvDecoder.h" +#include "NvEncoder/NvEncoderCuda.h" +#include "Utils/Logger.h" + +simplelogger::Logger* logger = simplelogger::LoggerFactory::CreateConsoleLogger(ERROR); + +namespace { + +struct Options { + std::string inputPath; + std::string outputPath = "recordly-nvidia-cuda-compositor.h264"; + std::string sourcePtsPath; + int fps = 30; + int maxFrames = 0; + int inputFrames = 0; + int targetFrames = 0; + int bitrateMbps = 18; + bool postSelect = false; + bool callbackEncode = false; + bool streamSync = false; + int prewarmMs = 0; + int chunkMb = 4; + int contentX = 0; + int contentY = 0; + int contentWidth = 0; + int contentHeight = 0; + int radius = 0; + int backgroundY = 16; + int backgroundU = 128; + int backgroundV = 128; + std::string backgroundNv12Path; + int shadowOffsetY = 0; + int shadowIntensityPct = 0; + std::string webcamNv12Path; + std::string webcamAnnexbPath; + int webcamInputFrames = 0; + int webcamTargetFrames = 0; + int webcamSourceWidth = 0; + int webcamSourceHeight = 0; + int webcamX = 0; + int webcamY = 0; + int webcamSize = 0; + int webcamRadius = 0; + bool webcamMirror = false; + std::string cursorSamplesPath; + int cursorHeight = 0; + std::string cursorAtlasRgbaPath; + std::string cursorAtlasMetadataPath; + int cursorAtlasWidth = 0; + int cursorAtlasHeight = 0; + std::string zoomSamplesPath; +}; + +constexpr int kMaxCursorAtlasEntries = 16; +constexpr int kWebcamPrefetchOutputFrames = 900; + +[[noreturn]] void fail(const std::string& message) { + throw std::runtime_error(message); +} + +void checkCuda(cudaError_t status, const char* expression) { + if (status != cudaSuccess) { + std::ostringstream stream; + stream << expression << " failed: " << cudaGetErrorString(status); + fail(stream.str()); + } +} + +void checkCu(CUresult status, const char* expression) { + if (status != CUDA_SUCCESS) { + const char* name = nullptr; + const char* message = nullptr; + cuGetErrorName(status, &name); + cuGetErrorString(status, &message); + std::ostringstream stream; + stream << expression << " failed: " << (name ? name : "CUDA_ERROR") + << " (" << (message ? message : "no detail") << ")"; + fail(stream.str()); + } +} + +int parsePositiveInt(const char* value, const char* name) { + char* end = nullptr; + const long parsed = std::strtol(value, &end, 10); + if (!end || *end != '\0' || parsed <= 0 || parsed > 1000000) { + std::ostringstream stream; + stream << "Invalid " << name << ": " << value; + fail(stream.str()); + } + return static_cast(parsed); +} + +int parseNonNegativeInt(const char* value, const char* name) { + char* end = nullptr; + const long parsed = std::strtol(value, &end, 10); + if (!end || *end != '\0' || parsed < 0 || parsed > 1000000) { + std::ostringstream stream; + stream << "Invalid " << name << ": " << value; + fail(stream.str()); + } + return static_cast(parsed); +} + +Options parseOptions(int argc, char** argv) { + Options options; + for (int index = 1; index < argc; ++index) { + const std::string arg = argv[index]; + auto requireValue = [&](const char* name) -> const char* { + if (index + 1 >= argc) { + std::ostringstream stream; + stream << "Missing value for " << name; + fail(stream.str()); + } + return argv[++index]; + }; + + if (arg == "--input") { + options.inputPath = requireValue("--input"); + } else if (arg == "--output") { + options.outputPath = requireValue("--output"); + } else if (arg == "--source-pts") { + options.sourcePtsPath = requireValue("--source-pts"); + } else if (arg == "--fps") { + options.fps = parsePositiveInt(requireValue("--fps"), "--fps"); + } else if (arg == "--max-frames") { + options.maxFrames = parsePositiveInt(requireValue("--max-frames"), "--max-frames"); + } else if (arg == "--input-frames") { + options.inputFrames = parsePositiveInt(requireValue("--input-frames"), "--input-frames"); + } else if (arg == "--target-frames") { + options.targetFrames = parsePositiveInt(requireValue("--target-frames"), "--target-frames"); + } else if (arg == "--bitrate-mbps") { + options.bitrateMbps = parsePositiveInt(requireValue("--bitrate-mbps"), "--bitrate-mbps"); + } else if (arg == "--post-select") { + options.postSelect = true; + } else if (arg == "--callback-encode") { + options.callbackEncode = true; + } else if (arg == "--stream-sync") { + options.streamSync = true; + } else if (arg == "--prewarm-ms") { + options.prewarmMs = parsePositiveInt(requireValue("--prewarm-ms"), "--prewarm-ms"); + } else if (arg == "--chunk-mb") { + options.chunkMb = parsePositiveInt(requireValue("--chunk-mb"), "--chunk-mb"); + } else if (arg == "--content-x") { + options.contentX = parseNonNegativeInt(requireValue("--content-x"), "--content-x"); + } else if (arg == "--content-y") { + options.contentY = parseNonNegativeInt(requireValue("--content-y"), "--content-y"); + } else if (arg == "--content-width") { + options.contentWidth = parsePositiveInt(requireValue("--content-width"), "--content-width"); + } else if (arg == "--content-height") { + options.contentHeight = parsePositiveInt(requireValue("--content-height"), "--content-height"); + } else if (arg == "--radius") { + options.radius = parseNonNegativeInt(requireValue("--radius"), "--radius"); + } else if (arg == "--background-y") { + options.backgroundY = parseNonNegativeInt(requireValue("--background-y"), "--background-y"); + } else if (arg == "--background-u") { + options.backgroundU = parseNonNegativeInt(requireValue("--background-u"), "--background-u"); + } else if (arg == "--background-v") { + options.backgroundV = parseNonNegativeInt(requireValue("--background-v"), "--background-v"); + } else if (arg == "--background-nv12") { + options.backgroundNv12Path = requireValue("--background-nv12"); + } else if (arg == "--shadow-offset-y") { + options.shadowOffsetY = parseNonNegativeInt(requireValue("--shadow-offset-y"), "--shadow-offset-y"); + } else if (arg == "--shadow-intensity-pct") { + options.shadowIntensityPct = parseNonNegativeInt(requireValue("--shadow-intensity-pct"), "--shadow-intensity-pct"); + } else if (arg == "--webcam-nv12") { + options.webcamNv12Path = requireValue("--webcam-nv12"); + } else if (arg == "--webcam-annexb") { + options.webcamAnnexbPath = requireValue("--webcam-annexb"); + } else if (arg == "--webcam-input-frames") { + options.webcamInputFrames = parsePositiveInt(requireValue("--webcam-input-frames"), "--webcam-input-frames"); + } else if (arg == "--webcam-target-frames") { + options.webcamTargetFrames = + parsePositiveInt(requireValue("--webcam-target-frames"), "--webcam-target-frames"); + } else if (arg == "--webcam-source-width") { + options.webcamSourceWidth = parsePositiveInt(requireValue("--webcam-source-width"), "--webcam-source-width"); + } else if (arg == "--webcam-source-height") { + options.webcamSourceHeight = + parsePositiveInt(requireValue("--webcam-source-height"), "--webcam-source-height"); + } else if (arg == "--webcam-x") { + options.webcamX = parseNonNegativeInt(requireValue("--webcam-x"), "--webcam-x"); + } else if (arg == "--webcam-y") { + options.webcamY = parseNonNegativeInt(requireValue("--webcam-y"), "--webcam-y"); + } else if (arg == "--webcam-size") { + options.webcamSize = parsePositiveInt(requireValue("--webcam-size"), "--webcam-size"); + } else if (arg == "--webcam-radius") { + options.webcamRadius = parseNonNegativeInt(requireValue("--webcam-radius"), "--webcam-radius"); + } else if (arg == "--webcam-mirror") { + options.webcamMirror = true; + } else if (arg == "--cursor-samples") { + options.cursorSamplesPath = requireValue("--cursor-samples"); + } else if (arg == "--cursor-height") { + options.cursorHeight = parsePositiveInt(requireValue("--cursor-height"), "--cursor-height"); + } else if (arg == "--cursor-atlas-rgba") { + options.cursorAtlasRgbaPath = requireValue("--cursor-atlas-rgba"); + } else if (arg == "--cursor-atlas-metadata") { + options.cursorAtlasMetadataPath = requireValue("--cursor-atlas-metadata"); + } else if (arg == "--cursor-atlas-width") { + options.cursorAtlasWidth = parsePositiveInt(requireValue("--cursor-atlas-width"), "--cursor-atlas-width"); + } else if (arg == "--cursor-atlas-height") { + options.cursorAtlasHeight = + parsePositiveInt(requireValue("--cursor-atlas-height"), "--cursor-atlas-height"); + } else if (arg == "--zoom-samples") { + options.zoomSamplesPath = requireValue("--zoom-samples"); + } else if (arg == "--help") { + std::cout << "Usage: recordly-nvidia-cuda-compositor --input input.annexb.h264 " + "[--output out.h264] [--source-pts source-pts.csv] [--fps 30] " + "[--max-frames N] [--bitrate-mbps N] " + "[--post-select] [--callback-encode] [--stream-sync] [--prewarm-ms N] [--chunk-mb N] " + "[--content-x N --content-y N --content-width N --content-height N --radius N] " + "[--background-nv12 background.nv12] [--shadow-offset-y N --shadow-intensity-pct N] " + "[--webcam-nv12 webcam.nv12 --webcam-x N --webcam-y N --webcam-size N --webcam-radius N] " + "[--webcam-annexb webcam.h264 --webcam-input-frames N --webcam-target-frames N] " + "[--cursor-samples cursor.tsv --cursor-height N] " + "[--cursor-atlas-rgba cursor.rgba --cursor-atlas-metadata cursor.tsv " + "--cursor-atlas-width N --cursor-atlas-height N] " + "[--zoom-samples zoom.csv]\n"; + std::exit(0); + } else { + std::ostringstream stream; + stream << "Unknown argument: " << arg; + fail(stream.str()); + } + } + if (options.inputPath.empty()) { + fail("--input is required"); + } + return options; +} + +bool shouldEncodeFrame(int sourceFrameIndex, int encodedFrames, const Options& options) { + if (options.inputFrames <= 0 || options.targetFrames <= 0) { + return true; + } + if (encodedFrames >= options.targetFrames) { + return false; + } + + const int expectedEncodedFrames = + ((sourceFrameIndex + 1) * options.targetFrames + options.inputFrames - 1) / options.inputFrames; + return encodedFrames < expectedEncodedFrames; +} + +bool hasStaticLayout(const Options& options) { + return options.contentWidth > 0 && options.contentHeight > 0; +} + +bool hasWebcamOverlay(const Options& options) { + return (!options.webcamNv12Path.empty() || !options.webcamAnnexbPath.empty()) && options.webcamSize > 0; +} + +int webcamFrameIndexForOutputFrame(int outputFrameIndex, const Options& options) { + if (options.webcamInputFrames > 0 && options.webcamTargetFrames > 0) { + return static_cast( + (static_cast(outputFrameIndex) * options.webcamInputFrames) / options.webcamTargetFrames); + } + return outputFrameIndex; +} + +std::vector loadFramePts(const std::string& path) { + std::vector timestamps; + if (path.empty()) { + return timestamps; + } + + std::ifstream input(path); + if (!input) { + fail("Failed to open source PTS sidecar: " + path); + } + + std::string line; + double lastTimestamp = -std::numeric_limits::infinity(); + while (std::getline(input, line)) { + if (line.empty()) { + continue; + } + char* end = nullptr; + const double timestamp = std::strtod(line.c_str(), &end); + if (!end || *end != '\0' || !std::isfinite(timestamp) || timestamp < lastTimestamp) { + fail("Invalid source PTS sidecar entry: " + line); + } + timestamps.push_back(timestamp); + lastTimestamp = timestamp; + } + + return timestamps; +} + +int maxSelectedFramesForTimeline(int targetFrames, int maxFrames) { + if (targetFrames <= 0) { + return maxFrames > 0 ? maxFrames : std::numeric_limits::max(); + } + return maxFrames > 0 ? std::min(maxFrames, targetFrames) : targetFrames; +} + +int expectedOutputFramesForSourceFrame( + int sourceFrameIndex, + int inputFrames, + int targetFrames, + int maxFrames, + int fps, + const std::vector* sourcePts) { + const int maxOutputFrames = maxSelectedFramesForTimeline(targetFrames, maxFrames); + if (sourcePts && sourceFrameIndex >= 0 && sourceFrameIndex < static_cast(sourcePts->size())) { + if (inputFrames > 0 && sourceFrameIndex + 1 >= inputFrames) { + return maxOutputFrames; + } + const double frameTimeSec = std::max(0.0, (*sourcePts)[sourceFrameIndex]); + const int64_t expected = static_cast(std::floor(frameTimeSec * fps)) + 1; + return static_cast(std::min(std::max(1, expected), maxOutputFrames)); + } + if (inputFrames <= 0 || targetFrames <= 0) { + return maxOutputFrames; + } + + const int64_t expected = + (static_cast(sourceFrameIndex + 1) * targetFrames + inputFrames - 1) / inputFrames; + return static_cast(std::min(expected, maxOutputFrames)); +} + +unsigned char clampByte(int value) { + return static_cast(std::max(0, std::min(255, value))); +} + +struct FrameSelectionState { + int inputFrames = 0; + int targetFrames = 0; + int maxFrames = 0; + int sourceFrames = 0; + int selectedFrames = 0; + int fps = 30; + const std::vector* sourcePts = nullptr; +}; + +bool shouldCopyDisplayFrame(int displayFrameIndex, void* userData) { + auto* state = static_cast(userData); + state->sourceFrames = displayFrameIndex + 1; + + const int maxSelectedFrames = + state->maxFrames > 0 ? std::min(state->maxFrames, state->targetFrames) : state->targetFrames; + if (state->selectedFrames >= maxSelectedFrames) { + return false; + } + + // maxFrames is a smoke-test stop cap; it must not spread the sampled frames + // across the full source because that hides the true first-window performance. + const int expectedSelectedFrames = expectedOutputFramesForSourceFrame( + displayFrameIndex, + state->inputFrames, + state->targetFrames, + state->maxFrames, + state->fps, + state->sourcePts); + if (state->selectedFrames >= expectedSelectedFrames) { + return false; + } + + ++state->selectedFrames; + return true; +} + +double elapsedMs(std::chrono::steady_clock::time_point start, std::chrono::steady_clock::time_point end); +struct ProgressCounters { + double decodeWallMs = 0.0; + double encodeMs = 0.0; + double compositeMs = 0.0; + double nvencMs = 0.0; + double packetWriteMs = 0.0; + double webcamDecodeMs = 0.0; + double webcamCopyMs = 0.0; + int roiCompositeFrames = 0; + int monolithicCompositeFrames = 0; + int copyCompositeFrames = 0; +}; + +struct ProgressReportState { + std::chrono::steady_clock::time_point startedAt; + std::chrono::steady_clock::time_point lastReportAt; + int lastReportedFrame = 0; + ProgressCounters lastCounters; +}; + +void reportEncodingProgress( + int encodedFrames, + int totalFrames, + ProgressReportState& state, + const ProgressCounters& counters, + bool force = false); + +struct WebcamFrameCache { + std::vector frames; + double decodeMs = 0.0; + double copyMs = 0.0; + int sourceFrames = 0; + int baseFrameIndex = 0; + int decodedFrames = 0; + int peakFrames = 0; + int width = 0; + int height = 0; + + ~WebcamFrameCache() { + for (unsigned char* frame : frames) { + cudaFree(frame); + } + } + + void pushFrame(unsigned char* frame) { + frames.push_back(frame); + decodedFrames = baseFrameIndex + static_cast(frames.size()); + sourceFrames = decodedFrames; + peakFrames = std::max(peakFrames, static_cast(frames.size())); + } + + void dropBefore(int minFrameIndex) { + const int dropCount = std::min( + std::max(0, minFrameIndex - baseFrameIndex), + std::max(0, static_cast(frames.size()) - 1)); + if (dropCount <= 0) { + return; + } + for (int index = 0; index < dropCount; ++index) { + cudaFree(frames[index]); + } + frames.erase(frames.begin(), frames.begin() + dropCount); + baseFrameIndex += dropCount; + } + + const unsigned char* frameAt(int frameIndex) const { + if (frames.empty()) { + return nullptr; + } + const int clampedFrameIndex = + std::max(baseFrameIndex, std::min(frameIndex, baseFrameIndex + static_cast(frames.size()) - 1)); + return frames[clampedFrameIndex - baseFrameIndex]; + } +}; + +struct CursorSample { + double timeMs = 0.0; + double cx = 0.0; + double cy = 0.0; + int typeIndex = 0; + double bounceScale = 1.0; +}; + +struct CursorPosition { + bool visible = false; + double cx = 0.0; + double cy = 0.0; + int typeIndex = 0; + double bounceScale = 1.0; +}; + +struct CursorTrack { + std::vector samples; + + CursorPosition positionAt(double timeMs) const { + if (samples.empty()) { + return {}; + } + if (timeMs <= samples.front().timeMs) { + return {true, samples.front().cx, samples.front().cy, samples.front().typeIndex, samples.front().bounceScale}; + } + if (timeMs >= samples.back().timeMs) { + return {true, samples.back().cx, samples.back().cy, samples.back().typeIndex, samples.back().bounceScale}; + } + + int low = 0; + int high = static_cast(samples.size()) - 1; + while (low < high - 1) { + const int mid = (low + high) / 2; + if (samples[mid].timeMs <= timeMs) { + low = mid; + } else { + high = mid; + } + } + + const CursorSample& left = samples[low]; + const CursorSample& right = samples[high]; + const double span = right.timeMs - left.timeMs; + if (span <= 0.0) { + return {true, left.cx, left.cy, left.typeIndex, left.bounceScale}; + } + + const double t = (timeMs - left.timeMs) / span; + return { + true, + left.cx + (right.cx - left.cx) * t, + left.cy + (right.cy - left.cy) * t, + t < 0.5 ? left.typeIndex : right.typeIndex, + left.bounceScale + (right.bounceScale - left.bounceScale) * t, + }; + } +}; + +std::unique_ptr loadCursorTrack(const Options& options) { + if (options.cursorSamplesPath.empty()) { + return nullptr; + } + if (options.cursorHeight <= 0) { + fail("--cursor-height is required with --cursor-samples"); + } + + std::ifstream input(options.cursorSamplesPath); + if (!input) { + fail("Failed to open cursor samples: " + options.cursorSamplesPath); + } + + auto track = std::make_unique(); + std::string line; + while (std::getline(input, line)) { + if (line.empty()) { + continue; + } + std::istringstream row(line); + CursorSample sample; + if (!(row >> sample.timeMs >> sample.cx >> sample.cy)) { + continue; + } + if (!(row >> sample.typeIndex)) { + sample.typeIndex = 0; + } + if (!(row >> sample.bounceScale)) { + sample.bounceScale = 1.0; + } + if (sample.cx < -1.0 || sample.cx > 2.0 || sample.cy < -1.0 || sample.cy > 2.0) { + continue; + } + sample.typeIndex = std::max(0, std::min(kMaxCursorAtlasEntries - 1, sample.typeIndex)); + sample.bounceScale = std::max(0.5, std::min(2.0, sample.bounceScale)); + track->samples.push_back(sample); + } + if (track->samples.empty()) { + fail("No cursor samples were loaded: " + options.cursorSamplesPath); + } + return track; +} + +struct ZoomSample { + double timeMs = 0.0; + double scale = 1.0; + double x = 0.0; + double y = 0.0; +}; + +struct ZoomTrack { + std::vector samples; + + ZoomSample sampleAt(double timeMs) const { + if (samples.empty()) { + return {}; + } + if (timeMs <= samples.front().timeMs) { + return samples.front(); + } + if (timeMs >= samples.back().timeMs) { + return samples.back(); + } + + int low = 0; + int high = static_cast(samples.size()) - 1; + while (low < high - 1) { + const int mid = (low + high) / 2; + if (samples[mid].timeMs <= timeMs) { + low = mid; + } else { + high = mid; + } + } + + const ZoomSample& left = samples[low]; + const ZoomSample& right = samples[high]; + const double span = right.timeMs - left.timeMs; + if (span <= 0.0) { + return left; + } + + const double t = (timeMs - left.timeMs) / span; + return { + timeMs, + left.scale + (right.scale - left.scale) * t, + left.x + (right.x - left.x) * t, + left.y + (right.y - left.y) * t, + }; + } +}; + +std::unique_ptr loadZoomTrack(const Options& options) { + if (options.zoomSamplesPath.empty()) { + return nullptr; + } + + std::ifstream input(options.zoomSamplesPath); + if (!input) { + fail("Failed to open zoom samples: " + options.zoomSamplesPath); + } + + auto track = std::make_unique(); + std::string line; + while (std::getline(input, line)) { + if (line.empty()) { + continue; + } + std::replace(line.begin(), line.end(), ',', ' '); + std::istringstream row(line); + ZoomSample sample; + if (!(row >> sample.timeMs >> sample.scale >> sample.x >> sample.y)) { + continue; + } + if (!std::isfinite(sample.timeMs) || !std::isfinite(sample.scale) || + !std::isfinite(sample.x) || !std::isfinite(sample.y)) { + continue; + } + sample.timeMs = std::max(0.0, sample.timeMs); + sample.scale = std::max(0.01, sample.scale); + track->samples.push_back(sample); + } + if (track->samples.empty()) { + fail("No zoom samples were loaded: " + options.zoomSamplesPath); + } + std::sort(track->samples.begin(), track->samples.end(), [](const auto& left, const auto& right) { + return left.timeMs < right.timeMs; + }); + return track; +} + +struct CursorAtlasEntry { + int x = 0; + int y = 0; + int width = 0; + int height = 0; + double anchorX = 0.0; + double anchorY = 0.0; + double aspectRatio = 0.0; + bool valid = false; +}; + +struct WebcamCacheState { + WebcamFrameCache* cache = nullptr; +}; + +void cacheMappedWebcamFrame( + CUdeviceptr dpSrcFrame, + unsigned int nSrcPitch, + int width, + int height, + int surfaceHeight, + int64_t, + void* userData) { + auto* state = static_cast(userData); + if (state->cache->width == 0) { + state->cache->width = width; + state->cache->height = height; + } + if (width != state->cache->width || height != state->cache->height) { + std::ostringstream stream; + stream << "Decoded webcam frame size changed from " << state->cache->width << "x" << state->cache->height + << " to " << width << "x" << height; + fail(stream.str()); + } + + const auto copyStart = std::chrono::steady_clock::now(); + const size_t expectedBytes = static_cast(width) * static_cast(height) * 3 / 2; + unsigned char* frame = nullptr; + checkCuda(cudaMalloc(&frame, expectedBytes), "cudaMalloc webcam cached frame"); + + CUDA_MEMCPY2D copy = {}; + copy.srcMemoryType = CU_MEMORYTYPE_DEVICE; + copy.srcDevice = dpSrcFrame; + copy.srcPitch = nSrcPitch; + copy.dstMemoryType = CU_MEMORYTYPE_DEVICE; + copy.dstDevice = reinterpret_cast(frame); + copy.dstPitch = width; + copy.WidthInBytes = width; + copy.Height = height; + checkCu(cuMemcpy2D(©), "cuMemcpy2D webcam luma"); + + copy.srcDevice = dpSrcFrame + nSrcPitch * surfaceHeight; + copy.dstDevice = reinterpret_cast(frame + width * height); + copy.Height = height / 2; + checkCu(cuMemcpy2D(©), "cuMemcpy2D webcam chroma"); + + state->cache->pushFrame(frame); + const auto copyEnd = std::chrono::steady_clock::now(); + state->cache->copyMs += elapsedMs(copyStart, copyEnd); +} + +class WebcamStreamDecoder { +public: + WebcamStreamDecoder(CUcontext context, const Options& options) + : options_(options), + chunk_(static_cast(options.chunkMb) * 1024 * 1024) { + if (options_.webcamInputFrames <= 0 || options_.webcamTargetFrames <= 0) { + fail("--webcam-input-frames and --webcam-target-frames are required with --webcam-annexb"); + } + if (options_.webcamSourceWidth <= 0 || options_.webcamSourceHeight <= 0) { + fail("--webcam-source-width and --webcam-source-height are required with --webcam-annexb"); + } + + const int cropSide = std::min(options_.webcamSourceWidth, options_.webcamSourceHeight) & ~1; + const int cropLeft = ((options_.webcamSourceWidth - cropSide) / 2) & ~1; + const int cropTop = ((options_.webcamSourceHeight - cropSide) / 2) & ~1; + crop_ = Rect{cropLeft, cropTop, cropLeft + cropSide, cropTop + cropSide}; + + cacheState_.cache = &cache_; + decoder_ = + std::make_unique(context, 0, 0, true, cudaVideoCodec_H264, nullptr, true, true, &crop_, nullptr); + decoder_->SetMappedFrameHandler(cacheMappedWebcamFrame, &cacheState_); + + input_.open(options_.webcamAnnexbPath, std::ios::binary); + if (!input_) { + fail("Failed to open webcam input: " + options_.webcamAnnexbPath); + } + } + + WebcamFrameCache* cache() { + return &cache_; + } + + void ensureFrame(int frameIndex) { + if (frameIndex < 0) { + return; + } + while (!flushed_ && cache_.decodedFrames <= frameIndex) { + input_.read(reinterpret_cast(chunk_.data()), static_cast(chunk_.size())); + const int bytesRead = static_cast(input_.gcount()); + const auto decodeStart = std::chrono::steady_clock::now(); + if (bytesRead > 0) { + decoder_->Decode(chunk_.data(), bytesRead, &frames_, &returnedFrames_); + } else { + decoder_->Decode(nullptr, 0, &frames_, &returnedFrames_); + flushed_ = true; + } + const auto decodeEnd = std::chrono::steady_clock::now(); + cache_.decodeMs += elapsedMs(decodeStart, decodeEnd); + } + if (cache_.frames.empty()) { + fail("No webcam frames were decoded"); + } + } + + void dropBefore(int frameIndex) { + cache_.dropBefore(frameIndex); + } + +private: + const Options& options_; + WebcamFrameCache cache_; + WebcamCacheState cacheState_{}; + Rect crop_{}; + std::unique_ptr decoder_; + std::ifstream input_; + std::vector chunk_; + uint8_t** frames_ = nullptr; + int returnedFrames_ = 0; + bool flushed_ = false; +}; + +std::unique_ptr createWebcamStreamDecoder(CUcontext context, const Options& options) { + if (options.webcamAnnexbPath.empty()) { + return nullptr; + } + return std::make_unique(context, options); +} + +__global__ void copyNv12Kernel( + const unsigned char* src, + int srcPitch, + int srcWidth, + int srcHeight, + int srcSurfaceHeight, + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight) { + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + if (x >= dstWidth || y >= dstHeight) { + return; + } + + const int sx = min(srcWidth - 1, (x * srcWidth) / dstWidth); + const int sy = min(srcHeight - 1, (y * srcHeight) / dstHeight); + dst[y * dstPitch + x] = src[sy * srcPitch + sx]; + + if ((x % 2) == 0 && (y % 2) == 0) { + const int suvX = min(srcWidth - 2, ((x * srcWidth) / dstWidth) & ~1); + const int suvY = min((srcHeight / 2) - 1, (y * srcHeight / dstHeight) / 2); + const unsigned char* srcUv = src + srcPitch * srcSurfaceHeight + suvY * srcPitch + suvX; + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + dstUv[0] = srcUv[0]; + dstUv[1] = srcUv[1]; + } +} + +__global__ void fillNv12Kernel( + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight, + unsigned char yValue, + unsigned char uValue, + unsigned char vValue) { + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + if (x >= dstWidth || y >= dstHeight) { + return; + } + + dst[y * dstPitch + x] = yValue; + if ((x % 2) == 0 && (y % 2) == 0) { + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + dstUv[0] = uValue; + dstUv[1] = vValue; + } +} + +__device__ bool isInsideRoundedRect( + int x, + int y, + int left, + int top, + int width, + int height, + int radius) { + if (x < left || y < top || x >= left + width || y >= top + height) { + return false; + } + if (radius <= 0) { + return true; + } + + const int right = left + width - 1; + const int bottom = top + height - 1; + const int innerLeft = left + radius; + const int innerRight = right - radius; + const int innerTop = top + radius; + const int innerBottom = bottom - radius; + if ((x >= innerLeft && x <= innerRight) || (y >= innerTop && y <= innerBottom)) { + return true; + } + + const int cx = x < innerLeft ? innerLeft : innerRight; + const int cy = y < innerTop ? innerTop : innerBottom; + const int dx = x - cx; + const int dy = y - cy; + return dx * dx + dy * dy <= radius * radius; +} + +__global__ void overlayContentRectNv12Kernel( + const unsigned char* src, + int srcPitch, + int srcWidth, + int srcHeight, + int srcSurfaceHeight, + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight, + int contentX, + int contentY, + int contentWidth, + int contentHeight) { + const int localX = blockIdx.x * blockDim.x + threadIdx.x; + const int localY = blockIdx.y * blockDim.y + threadIdx.y; + if (localX >= contentWidth || localY >= contentHeight) { + return; + } + + const int x = contentX + localX; + const int y = contentY + localY; + if (x < 0 || y < 0 || x >= dstWidth || y >= dstHeight) { + return; + } + + const int srcX = min(srcWidth - 1, (localX * srcWidth) / contentWidth); + const int srcY = min(srcHeight - 1, (localY * srcHeight) / contentHeight); + dst[y * dstPitch + x] = src[srcY * srcPitch + srcX]; + + if ((x % 2) == 0 && (y % 2) == 0) { + const int localUvX = max(0, min(contentWidth - 1, localX + 1)); + const int localUvY = max(0, min(contentHeight - 1, localY + 1)); + const int srcUvX = min(srcWidth - 2, ((localUvX * srcWidth) / contentWidth) & ~1); + const int srcUvY = min((srcHeight / 2) - 1, ((localUvY * srcHeight) / contentHeight) / 2); + const unsigned char* srcUv = src + srcPitch * srcSurfaceHeight + srcUvY * srcPitch + srcUvX; + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + dstUv[0] = srcUv[0]; + dstUv[1] = srcUv[1]; + } +} + +__global__ void overlayContentTransformNv12Kernel( + const unsigned char* src, + int srcPitch, + int srcWidth, + int srcHeight, + int srcSurfaceHeight, + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight, + int regionX, + int regionY, + int regionWidth, + int regionHeight, + int contentX, + int contentY, + int contentWidth, + int contentHeight, + int radius, + float zoomScale, + float invZoomScale, + float srcScaleX, + float srcScaleY, + float zoomX, + float zoomY) { + const int localX = blockIdx.x * blockDim.x + threadIdx.x; + const int localY = blockIdx.y * blockDim.y + threadIdx.y; + if (localX >= regionWidth || localY >= regionHeight) { + return; + } + + const int x = regionX + localX; + const int y = regionY + localY; + if (x < 0 || y < 0 || x >= dstWidth || y >= dstHeight) { + return; + } + + const float layoutXf = (static_cast(x) - zoomX) * invZoomScale; + const float layoutYf = (static_cast(y) - zoomY) * invZoomScale; + const int layoutX = __float2int_rd(layoutXf); + const int layoutY = __float2int_rd(layoutYf); + if (!isInsideRoundedRect(layoutX, layoutY, contentX, contentY, contentWidth, contentHeight, radius)) { + return; + } + + const float localContentX = + fminf(static_cast(contentWidth - 1), fmaxf(0.0f, layoutXf - contentX)); + const float localContentY = + fminf(static_cast(contentHeight - 1), fmaxf(0.0f, layoutYf - contentY)); + const int sx = min(srcWidth - 1, __float2int_rd(localContentX * srcScaleX)); + const int sy = min(srcHeight - 1, __float2int_rd(localContentY * srcScaleY)); + dst[y * dstPitch + x] = src[sy * srcPitch + sx]; + + if ((x % 2) == 0 && (y % 2) == 0 && x + 1 < dstWidth && y + 1 < dstHeight) { + const float uvLayoutXf = (static_cast(x + 1) - zoomX) * invZoomScale; + const float uvLayoutYf = (static_cast(y + 1) - zoomY) * invZoomScale; + const int uvLayoutX = __float2int_rd(uvLayoutXf); + const int uvLayoutY = __float2int_rd(uvLayoutYf); + if (isInsideRoundedRect( + uvLayoutX, + uvLayoutY, + contentX, + contentY, + contentWidth, + contentHeight, + radius)) { + const float uvLocalContentX = + fminf(static_cast(contentWidth - 1), fmaxf(0.0f, uvLayoutXf - contentX)); + const float uvLocalContentY = + fminf(static_cast(contentHeight - 1), fmaxf(0.0f, uvLayoutYf - contentY)); + const int suvX = + min(srcWidth - 2, __float2int_rd(uvLocalContentX * srcScaleX) & ~1); + const int suvY = + min((srcHeight / 2) - 1, __float2int_rd(uvLocalContentY * srcScaleY) / 2); + const unsigned char* srcUv = src + srcPitch * srcSurfaceHeight + suvY * srcPitch + suvX; + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + dstUv[0] = srcUv[0]; + dstUv[1] = srcUv[1]; + } + } +} + +__global__ void restoreRoundedContentCornersNv12Kernel( + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight, + int contentX, + int contentY, + int contentWidth, + int contentHeight, + int radius, + unsigned char backgroundY, + unsigned char backgroundU, + unsigned char backgroundV, + const unsigned char* background) { + const int localX = blockIdx.x * blockDim.x + threadIdx.x; + const int localY = blockIdx.y * blockDim.y + threadIdx.y; + if (localX >= radius || localY >= radius) { + return; + } + + const int corner = blockIdx.z; + const bool right = corner == 1 || corner == 3; + const bool bottom = corner >= 2; + const int x = right ? contentX + contentWidth - radius + localX : contentX + localX; + const int y = bottom ? contentY + contentHeight - radius + localY : contentY + localY; + if (x < 0 || y < 0 || x >= dstWidth || y >= dstHeight) { + return; + } + + if (!isInsideRoundedRect(x, y, contentX, contentY, contentWidth, contentHeight, radius)) { + dst[y * dstPitch + x] = background ? background[y * dstWidth + x] : backgroundY; + } + + if ((x % 2) == 0 && (y % 2) == 0 && + !isInsideRoundedRect(x + 1, y + 1, contentX, contentY, contentWidth, contentHeight, radius)) { + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + if (background) { + const unsigned char* bgUv = background + dstWidth * dstHeight + (y / 2) * dstWidth + x; + dstUv[0] = bgUv[0]; + dstUv[1] = bgUv[1]; + } else { + dstUv[0] = backgroundU; + dstUv[1] = backgroundV; + } + } +} + +__device__ bool pointInCursorPolygon(float x, float y, bool inner) { + constexpr int kCount = 7; + const float outerX[kCount] = {2.0f, 61.0f, 45.0f, 57.0f, 38.0f, 27.0f, 13.0f}; + const float outerY[kCount] = {2.0f, 61.0f, 63.0f, 91.0f, 95.0f, 67.0f, 79.0f}; + const float innerX[kCount] = {10.0f, 52.0f, 37.0f, 49.0f, 40.0f, 28.0f, 18.0f}; + const float innerY[kCount] = {11.0f, 53.0f, 53.0f, 78.0f, 83.0f, 57.0f, 66.0f}; + const float* px = inner ? innerX : outerX; + const float* py = inner ? innerY : outerY; + + bool inside = false; + for (int index = 0, previous = kCount - 1; index < kCount; previous = index++) { + const bool crosses = ((py[index] > y) != (py[previous] > y)) && + (x < (px[previous] - px[index]) * (y - py[index]) / (py[previous] - py[index]) + px[index]); + if (crosses) { + inside = !inside; + } + } + return inside; +} + +__device__ int cursorMaskAt( + int x, + int y, + int cursorX, + int cursorY, + int cursorWidth, + int cursorHeight) { + if (cursorWidth <= 0 || cursorHeight <= 0 || x < cursorX || y < cursorY || + x >= cursorX + cursorWidth || y >= cursorY + cursorHeight) { + return 0; + } + + const float localX = static_cast(x - cursorX) * 64.0f / static_cast(cursorWidth); + const float localY = static_cast(y - cursorY) * 96.0f / static_cast(cursorHeight); + if (pointInCursorPolygon(localX, localY, true)) { + return 2; + } + if (pointInCursorPolygon(localX, localY, false)) { + return 1; + } + return 0; +} + +__device__ unsigned char clampByteDevice(int value) { + return static_cast(min(255, max(0, value))); +} + +__device__ unsigned char blendByte(unsigned char base, unsigned char overlay, int alpha) { + return static_cast( + (static_cast(base) * (255 - alpha) + static_cast(overlay) * alpha + 127) / 255); +} + +__device__ bool sampleCursorAtlasNv12( + const unsigned char* atlas, + int atlasWidth, + int atlasHeight, + int entryX, + int entryY, + int entryWidth, + int entryHeight, + int cursorX, + int cursorY, + int cursorWidth, + int cursorHeight, + int x, + int y, + unsigned char* outY, + unsigned char* outU, + unsigned char* outV, + int* outAlpha) { + if (!atlas || cursorWidth <= 0 || cursorHeight <= 0 || entryWidth <= 0 || entryHeight <= 0 || + x < cursorX || y < cursorY || x >= cursorX + cursorWidth || y >= cursorY + cursorHeight) { + return false; + } + + const int localX = max(0, min(cursorWidth - 1, x - cursorX)); + const int localY = max(0, min(cursorHeight - 1, y - cursorY)); + const int sampleX = entryX + min(entryWidth - 1, (localX * entryWidth) / cursorWidth); + const int sampleY = entryY + min(entryHeight - 1, (localY * entryHeight) / cursorHeight); + if (sampleX < 0 || sampleY < 0 || sampleX >= atlasWidth || sampleY >= atlasHeight) { + return false; + } + + const int offset = (sampleY * atlasWidth + sampleX) * 4; + const int alpha = atlas[offset + 3]; + if (alpha <= 0) { + return false; + } + + const int r = atlas[offset]; + const int g = atlas[offset + 1]; + const int b = atlas[offset + 2]; + *outY = clampByteDevice(((66 * r + 129 * g + 25 * b + 128) >> 8) + 16); + *outU = clampByteDevice(((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128); + *outV = clampByteDevice(((112 * r - 94 * g - 18 * b + 128) >> 8) + 128); + *outAlpha = alpha; + return true; +} + +__device__ int sampleCursorAtlasAlpha( + const unsigned char* atlas, + int atlasWidth, + int atlasHeight, + int entryX, + int entryY, + int entryWidth, + int entryHeight, + int cursorX, + int cursorY, + int cursorWidth, + int cursorHeight, + int x, + int y) { + if (!atlas || cursorWidth <= 0 || cursorHeight <= 0 || entryWidth <= 0 || entryHeight <= 0 || + x < cursorX || y < cursorY || x >= cursorX + cursorWidth || y >= cursorY + cursorHeight) { + return 0; + } + + const int localX = max(0, min(cursorWidth - 1, x - cursorX)); + const int localY = max(0, min(cursorHeight - 1, y - cursorY)); + const int sampleX = entryX + min(entryWidth - 1, (localX * entryWidth) / cursorWidth); + const int sampleY = entryY + min(entryHeight - 1, (localY * entryHeight) / cursorHeight); + if (sampleX < 0 || sampleY < 0 || sampleX >= atlasWidth || sampleY >= atlasHeight) { + return 0; + } + + return atlas[(sampleY * atlasWidth + sampleX) * 4 + 3]; +} + +__device__ int sampleCursorAtlasShadowAlpha( + const unsigned char* atlas, + int atlasWidth, + int atlasHeight, + int entryX, + int entryY, + int entryWidth, + int entryHeight, + int cursorX, + int cursorY, + int cursorWidth, + int cursorHeight, + int x, + int y) { + int weightedAlpha = 0; + weightedAlpha += sampleCursorAtlasAlpha( + atlas, + atlasWidth, + atlasHeight, + entryX, + entryY, + entryWidth, + entryHeight, + cursorX, + cursorY + 2, + cursorWidth, + cursorHeight, + x, + y) * 20; + weightedAlpha += sampleCursorAtlasAlpha( + atlas, + atlasWidth, + atlasHeight, + entryX, + entryY, + entryWidth, + entryHeight, + cursorX - 2, + cursorY + 2, + cursorWidth, + cursorHeight, + x, + y) * 6; + weightedAlpha += sampleCursorAtlasAlpha( + atlas, + atlasWidth, + atlasHeight, + entryX, + entryY, + entryWidth, + entryHeight, + cursorX + 2, + cursorY + 2, + cursorWidth, + cursorHeight, + x, + y) * 6; + weightedAlpha += sampleCursorAtlasAlpha( + atlas, + atlasWidth, + atlasHeight, + entryX, + entryY, + entryWidth, + entryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x, + y) * 4; + weightedAlpha += sampleCursorAtlasAlpha( + atlas, + atlasWidth, + atlasHeight, + entryX, + entryY, + entryWidth, + entryHeight, + cursorX, + cursorY + 4, + cursorWidth, + cursorHeight, + x, + y) * 4; + return min(255, weightedAlpha / 100); +} + +__global__ void compositeStaticNv12Kernel( + const unsigned char* src, + int srcPitch, + int srcWidth, + int srcHeight, + int srcSurfaceHeight, + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight, + int contentX, + int contentY, + int contentWidth, + int contentHeight, + int radius, + unsigned char backgroundY, + unsigned char backgroundU, + unsigned char backgroundV, + const unsigned char* background, + int shadowOffsetY, + int shadowIntensityPct, + const unsigned char* webcam, + int webcamX, + int webcamY, + int webcamSize, + int webcamFrameWidth, + int webcamFrameHeight, + int webcamRadius, + bool webcamMirror, + bool cursorVisible, + int cursorX, + int cursorY, + int cursorWidth, + int cursorHeight, + const unsigned char* cursorAtlasRgba, + int cursorAtlasWidth, + int cursorAtlasHeight, + int cursorAtlasEntryX, + int cursorAtlasEntryY, + int cursorAtlasEntryWidth, + int cursorAtlasEntryHeight, + bool zoomEnabled, + float zoomScale, + float zoomX, + float zoomY) { + const int x = blockIdx.x * blockDim.x + threadIdx.x; + const int y = blockIdx.y * blockDim.y + threadIdx.y; + if (x >= dstWidth || y >= dstHeight) { + return; + } + + const bool zoomActive = zoomEnabled && zoomScale > 0.01f; + const float safeZoomScale = fmaxf(zoomScale, 0.01f); + const float layoutXf = zoomActive ? (static_cast(x) - zoomX) / safeZoomScale : static_cast(x); + const float layoutYf = zoomActive ? (static_cast(y) - zoomY) / safeZoomScale : static_cast(y); + const int layoutX = static_cast(floorf(layoutXf)); + const int layoutY = static_cast(floorf(layoutYf)); + + const bool inside = isInsideRoundedRect(layoutX, layoutY, contentX, contentY, contentWidth, contentHeight, radius); + unsigned char outY = background ? background[y * dstWidth + x] : backgroundY; + if (inside) { + const float localX = fminf(static_cast(contentWidth - 1), fmaxf(0.0f, layoutXf - contentX)); + const float localY = fminf(static_cast(contentHeight - 1), fmaxf(0.0f, layoutYf - contentY)); + const int sx = min(srcWidth - 1, static_cast((localX * srcWidth) / contentWidth)); + const int sy = min(srcHeight - 1, static_cast((localY * srcHeight) / contentHeight)); + outY = src[sy * srcPitch + sx]; + } else { + const bool shadowInside = + shadowIntensityPct > 0 && + isInsideRoundedRect( + layoutX, + layoutY, + contentX, + contentY + shadowOffsetY, + contentWidth, + contentHeight, + radius + 8); + if (shadowInside) { + const int darkenPct = min(75, max(0, shadowIntensityPct / 2)); + outY = static_cast((static_cast(outY) * (100 - darkenPct)) / 100); + } + } + if (webcam && isInsideRoundedRect(x, y, webcamX, webcamY, webcamSize, webcamSize, webcamRadius)) { + const int localX = max(0, min(webcamSize - 1, x - webcamX)); + const int localY = max(0, min(webcamSize - 1, y - webcamY)); + const int sampleX = min(webcamFrameWidth - 1, (localX * webcamFrameWidth) / webcamSize); + const int sampleY = min(webcamFrameHeight - 1, (localY * webcamFrameHeight) / webcamSize); + const int mirroredX = webcamMirror ? webcamFrameWidth - 1 - sampleX : sampleX; + outY = webcam[sampleY * webcamFrameWidth + mirroredX]; + } + unsigned char cursorYValue = 0; + unsigned char cursorUValue = 128; + unsigned char cursorVValue = 128; + int cursorAlpha = 0; + const int cursorShadowAlpha = cursorVisible && cursorAtlasRgba + ? sampleCursorAtlasShadowAlpha( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x, + y) + : 0; + if (cursorShadowAlpha > 0) { + outY = blendByte(outY, 16, cursorShadowAlpha); + } + const bool cursorAtlasHit = + cursorVisible && + sampleCursorAtlasNv12( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x, + y, + &cursorYValue, + &cursorUValue, + &cursorVValue, + &cursorAlpha); + if (cursorAtlasHit) { + outY = blendByte(outY, cursorYValue, cursorAlpha); + } else { + const int cursorMask = cursorVisible && !cursorAtlasRgba + ? cursorMaskAt(x, y, cursorX, cursorY, cursorWidth, cursorHeight) + : 0; + if (cursorMask == 1) { + outY = 235; + } else if (cursorMask == 2) { + outY = 16; + } + } + dst[y * dstPitch + x] = outY; + + if ((x % 2) == 0 && (y % 2) == 0) { + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + const float uvLayoutXf = + zoomActive ? (static_cast(x + 1) - zoomX) / safeZoomScale : static_cast(x + 1); + const float uvLayoutYf = + zoomActive ? (static_cast(y + 1) - zoomY) / safeZoomScale : static_cast(y + 1); + const int uvLayoutX = static_cast(floorf(uvLayoutXf)); + const int uvLayoutY = static_cast(floorf(uvLayoutYf)); + const bool uvInside = isInsideRoundedRect( + uvLayoutX, + uvLayoutY, + contentX, + contentY, + contentWidth, + contentHeight, + radius); + if (uvInside) { + const float localX = fminf(static_cast(contentWidth - 1), fmaxf(0.0f, uvLayoutXf - contentX)); + const float localY = fminf(static_cast(contentHeight - 1), fmaxf(0.0f, uvLayoutYf - contentY)); + const int suvX = min(srcWidth - 2, (static_cast((localX * srcWidth) / contentWidth)) & ~1); + const int suvY = min((srcHeight / 2) - 1, static_cast(localY * srcHeight / contentHeight) / 2); + const unsigned char* srcUv = src + srcPitch * srcSurfaceHeight + suvY * srcPitch + suvX; + dstUv[0] = srcUv[0]; + dstUv[1] = srcUv[1]; + } else { + if (background) { + const unsigned char* bgUv = background + dstWidth * dstHeight + (y / 2) * dstWidth + x; + dstUv[0] = bgUv[0]; + dstUv[1] = bgUv[1]; + } else { + dstUv[0] = backgroundU; + dstUv[1] = backgroundV; + } + } + if (webcam && + isInsideRoundedRect( + x + 1, + y + 1, + webcamX, + webcamY, + webcamSize, + webcamSize, + webcamRadius)) { + const int localX = max(0, min(webcamSize - 1, x + 1 - webcamX)); + const int localY = max(0, min(webcamSize - 1, y + 1 - webcamY)); + const int sampleX = min(webcamFrameWidth - 1, (localX * webcamFrameWidth) / webcamSize); + const int sampleY = min(webcamFrameHeight - 1, (localY * webcamFrameHeight) / webcamSize); + const int mirroredX = webcamMirror ? webcamFrameWidth - 1 - sampleX : sampleX; + const int webcamUvX = min(webcamFrameWidth - 2, mirroredX & ~1); + const int webcamUvY = min((webcamFrameHeight / 2) - 1, sampleY / 2); + const unsigned char* webcamUv = + webcam + webcamFrameWidth * webcamFrameHeight + webcamUvY * webcamFrameWidth + webcamUvX; + dstUv[0] = webcamUv[0]; + dstUv[1] = webcamUv[1]; + } + unsigned char cursorUvY = 0; + unsigned char cursorUvU = 128; + unsigned char cursorUvV = 128; + int cursorUvAlpha = 0; + const int cursorUvShadowAlpha = cursorVisible && cursorAtlasRgba + ? sampleCursorAtlasShadowAlpha( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x + 1, + y + 1) + : 0; + if (cursorUvShadowAlpha > 0) { + dstUv[0] = blendByte(dstUv[0], 128, cursorUvShadowAlpha); + dstUv[1] = blendByte(dstUv[1], 128, cursorUvShadowAlpha); + } + const bool cursorAtlasUvHit = + cursorVisible && + sampleCursorAtlasNv12( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x + 1, + y + 1, + &cursorUvY, + &cursorUvU, + &cursorUvV, + &cursorUvAlpha); + if (cursorAtlasUvHit) { + dstUv[0] = blendByte(dstUv[0], cursorUvU, cursorUvAlpha); + dstUv[1] = blendByte(dstUv[1], cursorUvV, cursorUvAlpha); + } else { + const int cursorUvMask = + cursorVisible && !cursorAtlasRgba + ? cursorMaskAt(x + 1, y + 1, cursorX, cursorY, cursorWidth, cursorHeight) + : 0; + if (cursorUvMask > 0) { + dstUv[0] = 128; + dstUv[1] = 128; + } + } + } +} + +__global__ void overlayWebcamNv12Kernel( + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight, + const unsigned char* webcam, + int regionX, + int regionY, + int regionWidth, + int regionHeight, + int webcamX, + int webcamY, + int webcamSize, + int webcamFrameWidth, + int webcamFrameHeight, + int webcamRadius, + bool webcamMirror) { + const int localX = blockIdx.x * blockDim.x + threadIdx.x; + const int localY = blockIdx.y * blockDim.y + threadIdx.y; + if (!webcam || localX >= regionWidth || localY >= regionHeight) { + return; + } + + const int x = regionX + localX; + const int y = regionY + localY; + if (x < 0 || y < 0 || x >= dstWidth || y >= dstHeight) { + return; + } + + if (isInsideRoundedRect(x, y, webcamX, webcamY, webcamSize, webcamSize, webcamRadius)) { + const int webcamLocalX = max(0, min(webcamSize - 1, x - webcamX)); + const int webcamLocalY = max(0, min(webcamSize - 1, y - webcamY)); + const int sampleX = min(webcamFrameWidth - 1, (webcamLocalX * webcamFrameWidth) / webcamSize); + const int sampleY = min(webcamFrameHeight - 1, (webcamLocalY * webcamFrameHeight) / webcamSize); + const int mirroredX = webcamMirror ? webcamFrameWidth - 1 - sampleX : sampleX; + dst[y * dstPitch + x] = webcam[sampleY * webcamFrameWidth + mirroredX]; + } + + if ((x % 2) == 0 && (y % 2) == 0 && x + 1 < dstWidth && y + 1 < dstHeight && + isInsideRoundedRect(x + 1, y + 1, webcamX, webcamY, webcamSize, webcamSize, webcamRadius)) { + const int uvLocalX = max(0, min(webcamSize - 1, x + 1 - webcamX)); + const int uvLocalY = max(0, min(webcamSize - 1, y + 1 - webcamY)); + const int uvSampleX = min(webcamFrameWidth - 1, (uvLocalX * webcamFrameWidth) / webcamSize); + const int uvSampleY = min(webcamFrameHeight - 1, (uvLocalY * webcamFrameHeight) / webcamSize); + const int uvMirroredX = webcamMirror ? webcamFrameWidth - 1 - uvSampleX : uvSampleX; + const int webcamUvX = min(webcamFrameWidth - 2, uvMirroredX & ~1); + const int webcamUvY = min((webcamFrameHeight / 2) - 1, uvSampleY / 2); + const unsigned char* webcamUv = + webcam + webcamFrameWidth * webcamFrameHeight + webcamUvY * webcamFrameWidth + webcamUvX; + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + dstUv[0] = webcamUv[0]; + dstUv[1] = webcamUv[1]; + } +} + +__global__ void overlayCursorNv12Kernel( + unsigned char* dst, + int dstPitch, + int dstChromaOffset, + int dstWidth, + int dstHeight, + int regionX, + int regionY, + int regionWidth, + int regionHeight, + bool cursorVisible, + int cursorX, + int cursorY, + int cursorWidth, + int cursorHeight, + const unsigned char* cursorAtlasRgba, + int cursorAtlasWidth, + int cursorAtlasHeight, + int cursorAtlasEntryX, + int cursorAtlasEntryY, + int cursorAtlasEntryWidth, + int cursorAtlasEntryHeight) { + const int localX = blockIdx.x * blockDim.x + threadIdx.x; + const int localY = blockIdx.y * blockDim.y + threadIdx.y; + if (!cursorVisible || localX >= regionWidth || localY >= regionHeight) { + return; + } + + const int x = regionX + localX; + const int y = regionY + localY; + if (x < 0 || y < 0 || x >= dstWidth || y >= dstHeight) { + return; + } + + unsigned char outY = dst[y * dstPitch + x]; + unsigned char cursorYValue = 0; + unsigned char cursorUValue = 128; + unsigned char cursorVValue = 128; + int cursorAlpha = 0; + const int cursorShadowAlpha = cursorAtlasRgba + ? sampleCursorAtlasShadowAlpha( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x, + y) + : 0; + if (cursorShadowAlpha > 0) { + outY = blendByte(outY, 16, cursorShadowAlpha); + } + const bool cursorAtlasHit = + sampleCursorAtlasNv12( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x, + y, + &cursorYValue, + &cursorUValue, + &cursorVValue, + &cursorAlpha); + if (cursorAtlasHit) { + outY = blendByte(outY, cursorYValue, cursorAlpha); + } else { + const int cursorMask = !cursorAtlasRgba + ? cursorMaskAt(x, y, cursorX, cursorY, cursorWidth, cursorHeight) + : 0; + if (cursorMask == 1) { + outY = 235; + } else if (cursorMask == 2) { + outY = 16; + } + } + dst[y * dstPitch + x] = outY; + + if ((x % 2) == 0 && (y % 2) == 0 && x + 1 < dstWidth && y + 1 < dstHeight) { + unsigned char* dstUv = dst + dstChromaOffset + (y / 2) * dstPitch + x; + unsigned char cursorUvY = 0; + unsigned char cursorUvU = 128; + unsigned char cursorUvV = 128; + int cursorUvAlpha = 0; + const int cursorUvShadowAlpha = cursorAtlasRgba + ? sampleCursorAtlasShadowAlpha( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x + 1, + y + 1) + : 0; + if (cursorUvShadowAlpha > 0) { + dstUv[0] = blendByte(dstUv[0], 128, cursorUvShadowAlpha); + dstUv[1] = blendByte(dstUv[1], 128, cursorUvShadowAlpha); + } + const bool cursorAtlasUvHit = + sampleCursorAtlasNv12( + cursorAtlasRgba, + cursorAtlasWidth, + cursorAtlasHeight, + cursorAtlasEntryX, + cursorAtlasEntryY, + cursorAtlasEntryWidth, + cursorAtlasEntryHeight, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + x + 1, + y + 1, + &cursorUvY, + &cursorUvU, + &cursorUvV, + &cursorUvAlpha); + if (cursorAtlasUvHit) { + dstUv[0] = blendByte(dstUv[0], cursorUvU, cursorUvAlpha); + dstUv[1] = blendByte(dstUv[1], cursorUvV, cursorUvAlpha); + } else { + const int cursorUvMask = + !cursorAtlasRgba ? cursorMaskAt(x + 1, y + 1, cursorX, cursorY, cursorWidth, cursorHeight) : 0; + if (cursorUvMask > 0) { + dstUv[0] = 128; + dstUv[1] = 128; + } + } + } +} + +__global__ void prewarmKernel(unsigned int* state, unsigned int seed) { + const unsigned int index = blockIdx.x * blockDim.x + threadIdx.x; + unsigned int value = seed ^ (index * 747796405u + 2891336453u); + for (int iteration = 0; iteration < 256; ++iteration) { + value = value * 1664525u + 1013904223u; + value ^= value >> 13; + } + state[index] = value; +} + +void prewarmCuda(int durationMs) { + if (durationMs <= 0) { + return; + } + + constexpr int blockSize = 256; + constexpr int blockCount = 256; + unsigned int* state = nullptr; + checkCuda(cudaMalloc(&state, blockSize * blockCount * sizeof(unsigned int)), "cudaMalloc prewarm"); + + const auto start = std::chrono::steady_clock::now(); + int iteration = 0; + while (elapsedMs(start, std::chrono::steady_clock::now()) < durationMs) { + prewarmKernel<<>>(state, static_cast(iteration++)); + checkCuda(cudaGetLastError(), "prewarmKernel"); + checkCuda(cudaDeviceSynchronize(), "cudaDeviceSynchronize prewarm"); + } + + checkCuda(cudaFree(state), "cudaFree prewarm"); +} + +class NvencSink { +public: + NvencSink( + CUcontext context, + int width, + int height, + int fps, + uint32_t bitrate, + const std::string& outputPath, + bool streamSync, + Options layoutOptions, + const WebcamFrameCache* webcamCache, + const CursorTrack* cursorTrack, + const ZoomTrack* zoomTrack) + : encoder_(context, width, height, NV_ENC_BUFFER_FORMAT_NV12), + width_(width), + height_(height), + fps_(fps), + streamSync_(streamSync), + layoutOptions_(layoutOptions), + webcamCache_(webcamCache), + cursorTrack_(cursorTrack), + zoomTrack_(zoomTrack) { + loadBackgroundFrame(); + loadWebcamFrame(); + loadCursorAtlas(); + if (streamSync_) { + checkCuda(cudaStreamCreateWithFlags(©Stream_, cudaStreamNonBlocking), "cudaStreamCreateWithFlags"); + } + + NV_ENC_INITIALIZE_PARAMS initializeParams = {NV_ENC_INITIALIZE_PARAMS_VER}; + NV_ENC_CONFIG encodeConfig = {NV_ENC_CONFIG_VER}; + initializeParams.encodeConfig = &encodeConfig; + encoder_.CreateDefaultEncoderParams(&initializeParams, NV_ENC_CODEC_H264_GUID, NV_ENC_PRESET_HP_GUID); + + initializeParams.frameRateNum = static_cast(fps); + initializeParams.frameRateDen = 1; + initializeParams.enableEncodeAsync = 1; + encodeConfig.profileGUID = NV_ENC_H264_PROFILE_HIGH_GUID; + encodeConfig.gopLength = static_cast(fps * 2); + encodeConfig.frameIntervalP = 1; + encodeConfig.rcParams.rateControlMode = NV_ENC_PARAMS_RC_VBR; + encodeConfig.rcParams.averageBitRate = bitrate; + encodeConfig.rcParams.maxBitRate = static_cast( + std::min(0xffffffffu, static_cast(bitrate) * 3 / 2)); + encodeConfig.rcParams.vbvBufferSize = bitrate; + encodeConfig.rcParams.vbvInitialDelay = bitrate; + encodeConfig.encodeCodecConfig.h264Config.idrPeriod = encodeConfig.gopLength; + encoder_.CreateEncoder(&initializeParams); + + output_.open(outputPath, std::ios::binary); + if (!output_) { + fail("Failed to open output: " + outputPath); + } + } + + void encodeFrame( + const unsigned char* srcFrame, + int srcPitch, + int srcWidth, + int srcHeight, + int srcSurfaceHeight, + int outputFrameIndex) { + const NvEncInputFrame* inputFrame = encoder_.GetNextInputFrame(); + const dim3 block(16, 16); + const dim3 grid((width_ + block.x - 1) / block.x, (height_ + block.y - 1) / block.y); + const unsigned char* webcamFrame = selectWebcamFrame(outputFrameIndex); + const double frameTimeMs = static_cast(outputFrameIndex) * 1000.0 / static_cast(fps_); + const ZoomSample zoomSample = zoomTrack_ ? zoomTrack_->sampleAt(frameTimeMs) : ZoomSample{}; + const bool zoomEnabled = zoomTrack_ && zoomSample.scale > 0.01; + const CursorPosition cursorPosition = cursorTrack_ + ? cursorTrack_->positionAt(frameTimeMs) + : CursorPosition{}; + const CursorAtlasEntry* cursorEntry = cursorAtlasEntryFor(cursorPosition.typeIndex); + const bool useCursorAtlas = cursorEntry && cursorAtlasDevice_; + const int cursorHeight = layoutOptions_.cursorHeight > 0 + ? std::max(1, static_cast(std::round(layoutOptions_.cursorHeight * cursorPosition.bounceScale))) + : 0; + const double cursorAspectRatio = useCursorAtlas ? cursorEntry->aspectRatio : (618.0 / 958.0); + const int cursorWidth = + cursorHeight > 0 ? std::max(1, static_cast(std::round(cursorHeight * cursorAspectRatio))) : 0; + const int cursorHotspotX = useCursorAtlas + ? static_cast(std::round(cursorWidth * cursorEntry->anchorX)) + : cursorWidth * 14 / 100; + const int cursorHotspotY = useCursorAtlas + ? static_cast(std::round(cursorHeight * cursorEntry->anchorY)) + : cursorHeight * 6 / 100; + const double cursorHotspotContentX = + layoutOptions_.contentX + cursorPosition.cx * layoutOptions_.contentWidth; + const double cursorHotspotContentY = + layoutOptions_.contentY + cursorPosition.cy * layoutOptions_.contentHeight; + const double cursorHotspotOutputX = + zoomEnabled ? cursorHotspotContentX * zoomSample.scale + zoomSample.x : cursorHotspotContentX; + const double cursorHotspotOutputY = + zoomEnabled ? cursorHotspotContentY * zoomSample.scale + zoomSample.y : cursorHotspotContentY; + const int cursorX = cursorPosition.visible + ? static_cast(std::round(cursorHotspotOutputX)) - cursorHotspotX + : 0; + const int cursorY = cursorPosition.visible + ? static_cast(std::round(cursorHotspotOutputY)) - cursorHotspotY + : 0; + const bool zoomChangesLayout = + zoomTrack_ && + (std::abs(zoomSample.scale - 1.0) > 0.001 || + std::abs(zoomSample.x) > 0.5 || + std::abs(zoomSample.y) > 0.5); + const bool useFastRoiComposite = + canUseFastRoiComposite(zoomChangesLayout); + const bool useLayeredStaticRoiComposite = + !useFastRoiComposite && canUseLayeredStaticRoiComposite(zoomChangesLayout); + const auto compositeStart = std::chrono::steady_clock::now(); + if (useFastRoiComposite) { + copyNv12Kernel<<>>( + srcFrame, + srcPitch, + srcWidth, + srcHeight, + srcSurfaceHeight, + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_); + checkCuda(cudaGetLastError(), "copyNv12Kernel fast ROI base"); + + if (webcamFrame && layoutOptions_.webcamSize > 0) { + const int webcamRegionX = std::max(0, layoutOptions_.webcamX - 1); + const int webcamRegionY = std::max(0, layoutOptions_.webcamY - 1); + const int webcamRegionRight = std::min( + width_, + layoutOptions_.webcamX + layoutOptions_.webcamSize); + const int webcamRegionBottom = std::min( + height_, + layoutOptions_.webcamY + layoutOptions_.webcamSize); + const int webcamRegionWidth = webcamRegionRight - webcamRegionX; + const int webcamRegionHeight = webcamRegionBottom - webcamRegionY; + if (webcamRegionWidth > 0 && webcamRegionHeight > 0) { + const dim3 webcamGrid( + (webcamRegionWidth + block.x - 1) / block.x, + (webcamRegionHeight + block.y - 1) / block.y); + overlayWebcamNv12Kernel<<>>( + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + webcamFrame, + webcamRegionX, + webcamRegionY, + webcamRegionWidth, + webcamRegionHeight, + layoutOptions_.webcamX, + layoutOptions_.webcamY, + layoutOptions_.webcamSize, + webcamFrameWidth(), + webcamFrameHeight(), + layoutOptions_.webcamRadius, + layoutOptions_.webcamMirror); + checkCuda(cudaGetLastError(), "overlayWebcamNv12Kernel"); + } + } + + if (cursorPosition.visible && cursorWidth > 0 && cursorHeight > 0) { + const int cursorPadding = useCursorAtlas ? 4 : 2; + const int regionX = std::max(0, cursorX - cursorPadding); + const int regionY = std::max(0, cursorY - cursorPadding); + const int regionRight = std::min(width_, cursorX + cursorWidth + cursorPadding); + const int regionBottom = std::min(height_, cursorY + cursorHeight + cursorPadding); + const int regionWidth = regionRight - regionX; + const int regionHeight = regionBottom - regionY; + if (regionWidth > 0 && regionHeight > 0) { + const dim3 cursorGrid( + (regionWidth + block.x - 1) / block.x, + (regionHeight + block.y - 1) / block.y); + overlayCursorNv12Kernel<<>>( + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + regionX, + regionY, + regionWidth, + regionHeight, + cursorPosition.visible, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + useCursorAtlas ? cursorAtlasDevice_ : nullptr, + cursorAtlasWidth_, + cursorAtlasHeight_, + useCursorAtlas ? cursorEntry->x : 0, + useCursorAtlas ? cursorEntry->y : 0, + useCursorAtlas ? cursorEntry->width : 0, + useCursorAtlas ? cursorEntry->height : 0); + checkCuda(cudaGetLastError(), "overlayCursorNv12Kernel"); + } + } + ++roiCompositeFrames_; + } else if (useLayeredStaticRoiComposite) { + if (backgroundDevice_) { + checkCuda( + cudaMemcpy2DAsync( + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + backgroundDevice_, + static_cast(width_), + static_cast(width_), + static_cast(height_), + cudaMemcpyDeviceToDevice, + copyStream_), + "cudaMemcpy2DAsync layered ROI background Y"); + checkCuda( + cudaMemcpy2DAsync( + static_cast(inputFrame->inputPtr) + + static_cast(inputFrame->chromaOffsets[0]), + static_cast(inputFrame->pitch), + backgroundDevice_ + width_ * height_, + static_cast(width_), + static_cast(width_), + static_cast(height_ / 2), + cudaMemcpyDeviceToDevice, + copyStream_), + "cudaMemcpy2DAsync layered ROI background UV"); + } else { + fillNv12Kernel<<>>( + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + clampByte(layoutOptions_.backgroundY), + clampByte(layoutOptions_.backgroundU), + clampByte(layoutOptions_.backgroundV)); + checkCuda(cudaGetLastError(), "fillNv12Kernel layered ROI background"); + } + + const float safeZoomScale = std::max(0.01f, static_cast(zoomSample.scale)); + const float invZoomScale = 1.0f / safeZoomScale; + const float srcScaleX = + static_cast(srcWidth) / static_cast(std::max(1, layoutOptions_.contentWidth)); + const float srcScaleY = + static_cast(srcHeight) / static_cast(std::max(1, layoutOptions_.contentHeight)); + const int transformedContentX = zoomChangesLayout + ? static_cast(std::floor(layoutOptions_.contentX * safeZoomScale + zoomSample.x)) + : layoutOptions_.contentX; + const int transformedContentY = zoomChangesLayout + ? static_cast(std::floor(layoutOptions_.contentY * safeZoomScale + zoomSample.y)) + : layoutOptions_.contentY; + const int transformedContentRight = zoomChangesLayout + ? static_cast( + std::ceil( + (layoutOptions_.contentX + layoutOptions_.contentWidth) * + safeZoomScale + + zoomSample.x)) + : layoutOptions_.contentX + layoutOptions_.contentWidth; + const int transformedContentBottom = zoomChangesLayout + ? static_cast( + std::ceil( + (layoutOptions_.contentY + layoutOptions_.contentHeight) * + safeZoomScale + + zoomSample.y)) + : layoutOptions_.contentY + layoutOptions_.contentHeight; + const int contentRegionLeft = std::max(0, transformedContentX); + const int contentRegionTop = std::max(0, transformedContentY); + const int contentRegionRight = std::min(width_, transformedContentRight); + const int contentRegionBottom = std::min(height_, transformedContentBottom); + const int contentRegionWidth = contentRegionRight - contentRegionLeft; + const int contentRegionHeight = contentRegionBottom - contentRegionTop; + if (contentRegionWidth > 0 && contentRegionHeight > 0) { + const dim3 contentGrid( + (contentRegionWidth + block.x - 1) / block.x, + (contentRegionHeight + block.y - 1) / block.y); + if (zoomChangesLayout) { + overlayContentTransformNv12Kernel<<>>( + srcFrame, + srcPitch, + srcWidth, + srcHeight, + srcSurfaceHeight, + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + contentRegionLeft, + contentRegionTop, + contentRegionWidth, + contentRegionHeight, + layoutOptions_.contentX, + layoutOptions_.contentY, + layoutOptions_.contentWidth, + layoutOptions_.contentHeight, + layoutOptions_.radius, + safeZoomScale, + invZoomScale, + srcScaleX, + srcScaleY, + static_cast(zoomSample.x), + static_cast(zoomSample.y)); + checkCuda(cudaGetLastError(), "overlayContentTransformNv12Kernel"); + } else { + overlayContentRectNv12Kernel<<>>( + srcFrame, + srcPitch, + srcWidth, + srcHeight, + srcSurfaceHeight, + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + layoutOptions_.contentX, + layoutOptions_.contentY, + layoutOptions_.contentWidth, + layoutOptions_.contentHeight); + checkCuda(cudaGetLastError(), "overlayContentRectNv12Kernel"); + + const int cornerRadius = std::min( + layoutOptions_.radius, + std::min(layoutOptions_.contentWidth, layoutOptions_.contentHeight) / 2); + if (cornerRadius > 0) { + const dim3 cornerGrid( + (cornerRadius + block.x - 1) / block.x, + (cornerRadius + block.y - 1) / block.y, + 4); + restoreRoundedContentCornersNv12Kernel<<>>( + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + layoutOptions_.contentX, + layoutOptions_.contentY, + layoutOptions_.contentWidth, + layoutOptions_.contentHeight, + cornerRadius, + clampByte(layoutOptions_.backgroundY), + clampByte(layoutOptions_.backgroundU), + clampByte(layoutOptions_.backgroundV), + backgroundDevice_); + checkCuda(cudaGetLastError(), "restoreRoundedContentCornersNv12Kernel"); + } + } + } + + if (webcamFrame && layoutOptions_.webcamSize > 0) { + const int webcamRegionX = std::max(0, layoutOptions_.webcamX - 1); + const int webcamRegionY = std::max(0, layoutOptions_.webcamY - 1); + const int webcamRegionRight = std::min( + width_, + layoutOptions_.webcamX + layoutOptions_.webcamSize); + const int webcamRegionBottom = std::min( + height_, + layoutOptions_.webcamY + layoutOptions_.webcamSize); + const int webcamRegionWidth = webcamRegionRight - webcamRegionX; + const int webcamRegionHeight = webcamRegionBottom - webcamRegionY; + if (webcamRegionWidth > 0 && webcamRegionHeight > 0) { + const dim3 webcamGrid( + (webcamRegionWidth + block.x - 1) / block.x, + (webcamRegionHeight + block.y - 1) / block.y); + overlayWebcamNv12Kernel<<>>( + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + webcamFrame, + webcamRegionX, + webcamRegionY, + webcamRegionWidth, + webcamRegionHeight, + layoutOptions_.webcamX, + layoutOptions_.webcamY, + layoutOptions_.webcamSize, + webcamFrameWidth(), + webcamFrameHeight(), + layoutOptions_.webcamRadius, + layoutOptions_.webcamMirror); + checkCuda(cudaGetLastError(), "overlayWebcamNv12Kernel layered ROI"); + } + } + + if (cursorPosition.visible && cursorWidth > 0 && cursorHeight > 0) { + const int cursorPadding = useCursorAtlas ? 4 : 2; + const int regionX = std::max(0, cursorX - cursorPadding); + const int regionY = std::max(0, cursorY - cursorPadding); + const int regionRight = std::min(width_, cursorX + cursorWidth + cursorPadding); + const int regionBottom = std::min(height_, cursorY + cursorHeight + cursorPadding); + const int regionWidth = regionRight - regionX; + const int regionHeight = regionBottom - regionY; + if (regionWidth > 0 && regionHeight > 0) { + const dim3 cursorGrid( + (regionWidth + block.x - 1) / block.x, + (regionHeight + block.y - 1) / block.y); + overlayCursorNv12Kernel<<>>( + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + regionX, + regionY, + regionWidth, + regionHeight, + cursorPosition.visible, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + useCursorAtlas ? cursorAtlasDevice_ : nullptr, + cursorAtlasWidth_, + cursorAtlasHeight_, + useCursorAtlas ? cursorEntry->x : 0, + useCursorAtlas ? cursorEntry->y : 0, + useCursorAtlas ? cursorEntry->width : 0, + useCursorAtlas ? cursorEntry->height : 0); + checkCuda(cudaGetLastError(), "overlayCursorNv12Kernel layered ROI"); + } + } + ++roiCompositeFrames_; + } else if (hasStaticLayout(layoutOptions_)) { + compositeStaticNv12Kernel<<>>( + srcFrame, + srcPitch, + srcWidth, + srcHeight, + srcSurfaceHeight, + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_, + layoutOptions_.contentX, + layoutOptions_.contentY, + layoutOptions_.contentWidth, + layoutOptions_.contentHeight, + layoutOptions_.radius, + clampByte(layoutOptions_.backgroundY), + clampByte(layoutOptions_.backgroundU), + clampByte(layoutOptions_.backgroundV), + backgroundDevice_, + layoutOptions_.shadowOffsetY, + layoutOptions_.shadowIntensityPct, + webcamFrame, + layoutOptions_.webcamX, + layoutOptions_.webcamY, + layoutOptions_.webcamSize, + webcamFrameWidth(), + webcamFrameHeight(), + layoutOptions_.webcamRadius, + layoutOptions_.webcamMirror, + cursorPosition.visible, + cursorX, + cursorY, + cursorWidth, + cursorHeight, + useCursorAtlas ? cursorAtlasDevice_ : nullptr, + cursorAtlasWidth_, + cursorAtlasHeight_, + useCursorAtlas ? cursorEntry->x : 0, + useCursorAtlas ? cursorEntry->y : 0, + useCursorAtlas ? cursorEntry->width : 0, + useCursorAtlas ? cursorEntry->height : 0, + zoomEnabled, + static_cast(zoomSample.scale), + static_cast(zoomSample.x), + static_cast(zoomSample.y)); + checkCuda(cudaGetLastError(), "compositeStaticNv12Kernel"); + ++monolithicCompositeFrames_; + } else { + copyNv12Kernel<<>>( + srcFrame, + srcPitch, + srcWidth, + srcHeight, + srcSurfaceHeight, + static_cast(inputFrame->inputPtr), + static_cast(inputFrame->pitch), + static_cast(inputFrame->chromaOffsets[0]), + width_, + height_); + checkCuda(cudaGetLastError(), "copyNv12Kernel"); + ++copyCompositeFrames_; + } + if (streamSync_) { + checkCuda(cudaStreamSynchronize(copyStream_), "cudaStreamSynchronize copy"); + } else { + checkCuda(cudaDeviceSynchronize(), "cudaDeviceSynchronize"); + } + const auto compositeEnd = std::chrono::steady_clock::now(); + compositeMs_ += elapsedMs(compositeStart, compositeEnd); + + std::vector> packets; + const auto nvencStart = std::chrono::steady_clock::now(); + encoder_.EncodeFrame(packets); + const auto nvencEnd = std::chrono::steady_clock::now(); + nvencMs_ += elapsedMs(nvencStart, nvencEnd); + const auto writeStart = std::chrono::steady_clock::now(); + writePackets(packets); + const auto writeEnd = std::chrono::steady_clock::now(); + packetWriteMs_ += elapsedMs(writeStart, writeEnd); + ++frames_; + } + + void finish() { + std::vector> packets; + encoder_.EndEncode(packets); + writePackets(packets); + encoder_.DestroyEncoder(); + output_.close(); + if (copyStream_) { + checkCuda(cudaStreamDestroy(copyStream_), "cudaStreamDestroy"); + copyStream_ = nullptr; + } + if (backgroundDevice_) { + checkCuda(cudaFree(backgroundDevice_), "cudaFree backgroundDevice"); + backgroundDevice_ = nullptr; + } + if (webcamDevice_) { + checkCuda(cudaFree(webcamDevice_), "cudaFree webcamDevice"); + webcamDevice_ = nullptr; + } + if (cursorAtlasDevice_) { + checkCuda(cudaFree(cursorAtlasDevice_), "cudaFree cursorAtlasDevice"); + cursorAtlasDevice_ = nullptr; + } + } + + uint64_t outputBytes() const { + return outputBytes_; + } + + double compositeMs() const { + return compositeMs_; + } + + double nvencMs() const { + return nvencMs_; + } + + double packetWriteMs() const { + return packetWriteMs_; + } + + int roiCompositeFrames() const { + return roiCompositeFrames_; + } + + int monolithicCompositeFrames() const { + return monolithicCompositeFrames_; + } + + int copyCompositeFrames() const { + return copyCompositeFrames_; + } + +private: + bool canUseFastRoiComposite(bool zoomChangesLayout) const { + return hasStaticLayout(layoutOptions_) && + layoutOptions_.contentX == 0 && + layoutOptions_.contentY == 0 && + layoutOptions_.contentWidth == width_ && + layoutOptions_.contentHeight == height_ && + layoutOptions_.radius == 0 && + layoutOptions_.shadowIntensityPct == 0 && + backgroundDevice_ == nullptr && + !zoomChangesLayout; + } + + bool canUseLayeredStaticRoiComposite(bool zoomChangesLayout) const { + return hasStaticLayout(layoutOptions_) && + layoutOptions_.contentWidth > 0 && + layoutOptions_.contentHeight > 0 && + layoutOptions_.contentX < width_ && + layoutOptions_.contentY < height_ && + layoutOptions_.contentX + layoutOptions_.contentWidth > 0 && + layoutOptions_.contentY + layoutOptions_.contentHeight > 0 && + layoutOptions_.shadowIntensityPct == 0; + } + + const unsigned char* selectWebcamFrame(int outputFrameIndex) const { + if (webcamCache_ && !webcamCache_->frames.empty()) { + return webcamCache_->frameAt(webcamFrameIndexForOutputFrame(outputFrameIndex, layoutOptions_)); + } + return webcamDevice_; + } + + int webcamFrameWidth() const { + return webcamCache_ ? webcamCache_->width : layoutOptions_.webcamSize; + } + + int webcamFrameHeight() const { + return webcamCache_ ? webcamCache_->height : layoutOptions_.webcamSize; + } + + void loadBackgroundFrame() { + if (layoutOptions_.backgroundNv12Path.empty()) { + return; + } + + const size_t expectedBytes = static_cast(width_) * static_cast(height_) * 3 / 2; + std::vector bytes(expectedBytes); + std::ifstream input(layoutOptions_.backgroundNv12Path, std::ios::binary); + if (!input) { + fail("Failed to open background NV12: " + layoutOptions_.backgroundNv12Path); + } + input.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (static_cast(input.gcount()) != expectedBytes) { + fail("Background NV12 has an unexpected size: " + layoutOptions_.backgroundNv12Path); + } + + checkCuda(cudaMalloc(&backgroundDevice_, expectedBytes), "cudaMalloc backgroundDevice"); + checkCuda( + cudaMemcpy(backgroundDevice_, bytes.data(), expectedBytes, cudaMemcpyHostToDevice), + "cudaMemcpy backgroundDevice"); + } + + void loadWebcamFrame() { + if (layoutOptions_.webcamNv12Path.empty()) { + return; + } + + const size_t expectedBytes = + static_cast(layoutOptions_.webcamSize) * static_cast(layoutOptions_.webcamSize) * 3 / 2; + std::vector bytes(expectedBytes); + std::ifstream input(layoutOptions_.webcamNv12Path, std::ios::binary); + if (!input) { + fail("Failed to open webcam NV12: " + layoutOptions_.webcamNv12Path); + } + input.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (static_cast(input.gcount()) != expectedBytes) { + fail("Webcam NV12 has an unexpected size: " + layoutOptions_.webcamNv12Path); + } + + checkCuda(cudaMalloc(&webcamDevice_, expectedBytes), "cudaMalloc webcamDevice"); + checkCuda( + cudaMemcpy(webcamDevice_, bytes.data(), expectedBytes, cudaMemcpyHostToDevice), + "cudaMemcpy webcamDevice"); + } + + const CursorAtlasEntry* cursorAtlasEntryFor(int typeIndex) const { + if (typeIndex < 0 || typeIndex >= kMaxCursorAtlasEntries) { + return nullptr; + } + const CursorAtlasEntry& entry = cursorAtlasEntries_[typeIndex]; + return entry.valid ? &entry : nullptr; + } + + void loadCursorAtlas() { + if (layoutOptions_.cursorAtlasRgbaPath.empty()) { + return; + } + if (layoutOptions_.cursorAtlasMetadataPath.empty() || + layoutOptions_.cursorAtlasWidth <= 0 || + layoutOptions_.cursorAtlasHeight <= 0) { + fail("Cursor atlas requires metadata, width, and height"); + } + + std::ifstream metadata(layoutOptions_.cursorAtlasMetadataPath); + if (!metadata) { + fail("Failed to open cursor atlas metadata: " + layoutOptions_.cursorAtlasMetadataPath); + } + + int loadedEntries = 0; + int index = 0; + CursorAtlasEntry entry; + while (metadata >> index >> entry.x >> entry.y >> entry.width >> entry.height >> + entry.anchorX >> entry.anchorY >> entry.aspectRatio) { + if (index < 0 || index >= kMaxCursorAtlasEntries || entry.width <= 0 || entry.height <= 0) { + continue; + } + entry.valid = true; + cursorAtlasEntries_[index] = entry; + ++loadedEntries; + } + if (loadedEntries == 0) { + fail("No cursor atlas entries were loaded: " + layoutOptions_.cursorAtlasMetadataPath); + } + + cursorAtlasWidth_ = layoutOptions_.cursorAtlasWidth; + cursorAtlasHeight_ = layoutOptions_.cursorAtlasHeight; + const size_t expectedBytes = + static_cast(cursorAtlasWidth_) * static_cast(cursorAtlasHeight_) * 4; + std::vector bytes(expectedBytes); + std::ifstream input(layoutOptions_.cursorAtlasRgbaPath, std::ios::binary); + if (!input) { + fail("Failed to open cursor atlas RGBA: " + layoutOptions_.cursorAtlasRgbaPath); + } + input.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (static_cast(input.gcount()) != expectedBytes) { + fail("Cursor atlas RGBA has an unexpected size: " + layoutOptions_.cursorAtlasRgbaPath); + } + + checkCuda(cudaMalloc(&cursorAtlasDevice_, expectedBytes), "cudaMalloc cursorAtlasDevice"); + checkCuda( + cudaMemcpy(cursorAtlasDevice_, bytes.data(), expectedBytes, cudaMemcpyHostToDevice), + "cudaMemcpy cursorAtlasDevice"); + } + + void writePackets(const std::vector>& packets) { + for (const auto& packet : packets) { + if (packet.empty()) { + continue; + } + output_.write(reinterpret_cast(packet.data()), static_cast(packet.size())); + outputBytes_ += packet.size(); + } + } + + NvEncoderCuda encoder_; + std::ofstream output_; + int width_ = 0; + int height_ = 0; + int fps_ = 30; + int frames_ = 0; + uint64_t outputBytes_ = 0; + double compositeMs_ = 0.0; + double nvencMs_ = 0.0; + double packetWriteMs_ = 0.0; + int roiCompositeFrames_ = 0; + int monolithicCompositeFrames_ = 0; + int copyCompositeFrames_ = 0; + bool streamSync_ = false; + Options layoutOptions_; + unsigned char* backgroundDevice_ = nullptr; + unsigned char* webcamDevice_ = nullptr; + unsigned char* cursorAtlasDevice_ = nullptr; + CursorAtlasEntry cursorAtlasEntries_[kMaxCursorAtlasEntries]; + int cursorAtlasWidth_ = 0; + int cursorAtlasHeight_ = 0; + const WebcamFrameCache* webcamCache_ = nullptr; + const CursorTrack* cursorTrack_ = nullptr; + const ZoomTrack* zoomTrack_ = nullptr; + cudaStream_t copyStream_ = nullptr; +}; + +struct CallbackEncodeState { + CUcontext context = nullptr; + const Options* options = nullptr; + uint32_t bitrate = 0; + std::unique_ptr* sink = nullptr; + double* decodeMs = nullptr; + double* encodeMs = nullptr; + int* encodedFrames = nullptr; + int displayFrameIndex = 0; + const WebcamFrameCache* webcamCache = nullptr; + const CursorTrack* cursorTrack = nullptr; + const ZoomTrack* zoomTrack = nullptr; + ProgressReportState* progress = nullptr; + bool oneFramePerMappedDisplayFrame = false; + int mappedFrames = 0; + const std::vector* sourcePts = nullptr; +}; + +ProgressCounters collectProgressCounters( + const NvencSink* sink, + const WebcamFrameCache* webcamCache, + double decodeWallMs, + double encodeMs) { + ProgressCounters counters; + counters.decodeWallMs = decodeWallMs; + counters.encodeMs = encodeMs; + if (sink) { + counters.compositeMs = sink->compositeMs(); + counters.nvencMs = sink->nvencMs(); + counters.packetWriteMs = sink->packetWriteMs(); + counters.roiCompositeFrames = sink->roiCompositeFrames(); + counters.monolithicCompositeFrames = sink->monolithicCompositeFrames(); + counters.copyCompositeFrames = sink->copyCompositeFrames(); + } + if (webcamCache) { + counters.webcamDecodeMs = webcamCache->decodeMs; + counters.webcamCopyMs = webcamCache->copyMs; + } + return counters; +} + +int maxCallbackOutputFrames(const Options& options) { + int maxOutputFrames = options.targetFrames > 0 + ? options.targetFrames + : std::numeric_limits::max(); + if (options.maxFrames > 0) { + maxOutputFrames = std::min(maxOutputFrames, options.maxFrames); + } + return maxOutputFrames; +} + +bool shouldContinueEncoding(int encodedFrames, const Options& options) { + return encodedFrames < maxCallbackOutputFrames(options); +} + +int expectedCallbackOutputFramesForSourceFrame( + int sourceFrameIndex, + const Options& options, + const std::vector* sourcePts) { + return expectedOutputFramesForSourceFrame( + sourceFrameIndex, + options.inputFrames, + options.targetFrames, + options.maxFrames, + options.fps, + sourcePts); +} + +void encodeMappedDisplayFrame( + CUdeviceptr dpSrcFrame, + unsigned int nSrcPitch, + int width, + int height, + int surfaceHeight, + int64_t, + void* userData) { + auto* state = static_cast(userData); + ++state->mappedFrames; + const int sourceFrameIndex = state->displayFrameIndex++; + const int maxOutputFrames = maxCallbackOutputFrames(*state->options); + if (*state->encodedFrames >= maxOutputFrames) { + return; + } + const int expectedOutputFrames = state->oneFramePerMappedDisplayFrame + ? *state->encodedFrames + 1 + : expectedCallbackOutputFramesForSourceFrame( + sourceFrameIndex, + *state->options, + state->sourcePts); + if (*state->encodedFrames >= expectedOutputFrames) { + return; + } + + if (!*state->sink) { + *state->sink = std::make_unique( + state->context, + width, + height, + state->options->fps, + state->bitrate, + state->options->outputPath, + state->options->streamSync, + *state->options, + state->webcamCache, + state->cursorTrack, + state->zoomTrack); + } + + while (*state->encodedFrames < expectedOutputFrames && *state->encodedFrames < maxOutputFrames) { + const auto encodeStart = std::chrono::steady_clock::now(); + (*state->sink)->encodeFrame( + reinterpret_cast(dpSrcFrame), + static_cast(nSrcPitch), + width, + height, + surfaceHeight, + *state->encodedFrames); + const auto encodeEnd = std::chrono::steady_clock::now(); + *state->encodeMs += elapsedMs(encodeStart, encodeEnd); + ++*state->encodedFrames; + if (state->progress) { + const NvencSink* activeSink = state->sink && *state->sink ? state->sink->get() : nullptr; + reportEncodingProgress( + *state->encodedFrames, + maxOutputFrames, + *state->progress, + collectProgressCounters( + activeSink, + state->webcamCache, + state->decodeMs ? *state->decodeMs : 0.0, + state->encodeMs ? *state->encodeMs : 0.0)); + } + } +} + +double elapsedMs(std::chrono::steady_clock::time_point start, std::chrono::steady_clock::time_point end) { + return std::chrono::duration(end - start).count(); +} + +void reportEncodingProgress( + int encodedFrames, + int totalFrames, + ProgressReportState& state, + const ProgressCounters& counters, + bool force) { + if (totalFrames <= 0) { + return; + } + + const auto now = std::chrono::steady_clock::now(); + if (!force && encodedFrames < totalFrames && encodedFrames % 30 != 0 && elapsedMs(state.lastReportAt, now) < 500.0) { + return; + } + + const double percentage = std::min(100.0, std::max(0.0, static_cast(encodedFrames) * 100.0 / totalFrames)); + const double elapsedSeconds = std::max(elapsedMs(state.startedAt, now) / 1000.0, 0.001); + const double averageFps = encodedFrames > 0 ? static_cast(encodedFrames) / elapsedSeconds : 0.0; + const double intervalMs = std::max(elapsedMs(state.lastReportAt, now), 0.0); + const int intervalFrames = std::max(0, encodedFrames - state.lastReportedFrame); + const double instantFps = + intervalMs > 0.0 && intervalFrames > 0 ? static_cast(intervalFrames) * 1000.0 / intervalMs : 0.0; + const double intervalEncodeMs = std::max(0.0, counters.encodeMs - state.lastCounters.encodeMs); + const double intervalCompositeMs = std::max(0.0, counters.compositeMs - state.lastCounters.compositeMs); + const double intervalNvencMs = std::max(0.0, counters.nvencMs - state.lastCounters.nvencMs); + const double intervalPacketWriteMs = std::max(0.0, counters.packetWriteMs - state.lastCounters.packetWriteMs); + const double intervalWebcamDecodeMs = std::max(0.0, counters.webcamDecodeMs - state.lastCounters.webcamDecodeMs); + const double intervalWebcamCopyMs = std::max(0.0, counters.webcamCopyMs - state.lastCounters.webcamCopyMs); + const double intervalDecodeWallMs = std::max(0.0, counters.decodeWallMs - state.lastCounters.decodeWallMs); + const double intervalPipelineWaitMs = std::max(0.0, intervalMs - intervalEncodeMs); + const int intervalRoiCompositeFrames = + std::max(0, counters.roiCompositeFrames - state.lastCounters.roiCompositeFrames); + const int intervalMonolithicCompositeFrames = + std::max(0, counters.monolithicCompositeFrames - state.lastCounters.monolithicCompositeFrames); + const int intervalCopyCompositeFrames = + std::max(0, counters.copyCompositeFrames - state.lastCounters.copyCompositeFrames); + std::cerr << std::fixed << std::setprecision(2) + << "PROGRESS {\"currentFrame\":" << encodedFrames + << ",\"totalFrames\":" << totalFrames + << ",\"percentage\":" << percentage + << ",\"averageFps\":" << averageFps + << ",\"instantFps\":" << instantFps + << ",\"intervalMs\":" << intervalMs + << ",\"intervalFrames\":" << intervalFrames + << ",\"intervalDecodeWallMs\":" << intervalDecodeWallMs + << ",\"intervalEncodeMs\":" << intervalEncodeMs + << ",\"intervalPipelineWaitMs\":" << intervalPipelineWaitMs + << ",\"intervalCompositeMs\":" << intervalCompositeMs + << ",\"intervalNvencMs\":" << intervalNvencMs + << ",\"intervalPacketWriteMs\":" << intervalPacketWriteMs + << ",\"intervalWebcamDecodeMs\":" << intervalWebcamDecodeMs + << ",\"intervalWebcamCopyMs\":" << intervalWebcamCopyMs + << ",\"intervalRoiCompositeFrames\":" << intervalRoiCompositeFrames + << ",\"intervalMonolithicCompositeFrames\":" << intervalMonolithicCompositeFrames + << ",\"intervalCopyCompositeFrames\":" << intervalCopyCompositeFrames + << "}" << std::endl; + state.lastReportAt = now; + state.lastReportedFrame = encodedFrames; + state.lastCounters = counters; +} + +} // namespace + +int main(int argc, char** argv) { + try { + const Options options = parseOptions(argc, argv); + const uint32_t bitrate = static_cast(options.bitrateMbps) * 1000U * 1000U; + + checkCuda(cudaSetDevice(0), "cudaSetDevice"); + checkCu(cuInit(0), "cuInit"); + CUdevice device = 0; + checkCu(cuDeviceGet(&device, 0), "cuDeviceGet"); + CUcontext context = nullptr; + checkCu(cuCtxCreate(&context, 0, device), "cuCtxCreate"); + checkCu(cuCtxSetCurrent(context), "cuCtxSetCurrent"); + prewarmCuda(options.prewarmMs); + + std::ifstream input(options.inputPath, std::ios::binary); + if (!input) { + fail("Failed to open input: " + options.inputPath); + } + + std::unique_ptr webcamStream = createWebcamStreamDecoder(context, options); + const WebcamFrameCache* webcamCachePtr = webcamStream ? webcamStream->cache() : nullptr; + std::unique_ptr cursorTrack = loadCursorTrack(options); + const CursorTrack* cursorTrackPtr = cursorTrack.get(); + std::unique_ptr zoomTrack = loadZoomTrack(options); + const ZoomTrack* zoomTrackPtr = zoomTrack.get(); + const std::vector sourcePts = loadFramePts(options.sourcePtsPath); + const bool useSourcePts = + options.inputFrames > 0 && + sourcePts.size() >= static_cast(options.inputFrames); + auto decoder = std::make_unique(context, 0, 0, true, cudaVideoCodec_H264, nullptr, true, true); + std::unique_ptr sink; + const bool useDecoderFramePolicy = + options.inputFrames > 0 && + options.targetFrames > 0 && + options.inputFrames >= options.targetFrames && + !useSourcePts && + !options.postSelect; + FrameSelectionState selectionState{ + options.inputFrames, + options.targetFrames, + options.maxFrames, + 0, + 0, + options.fps, + useSourcePts ? &sourcePts : nullptr, + }; + if (useDecoderFramePolicy) { + decoder->SetDisplayFramePolicy(shouldCopyDisplayFrame, &selectionState); + } + + std::vector chunk(static_cast(options.chunkMb) * 1024 * 1024); + uint8_t** frames = nullptr; + int returnedFrames = 0; + int sourceFrames = 0; + int encodedFrames = 0; + const auto totalStart = std::chrono::steady_clock::now(); + double decodeMs = 0.0; + double encodeMs = 0.0; + ProgressReportState progressState; + progressState.startedAt = std::chrono::steady_clock::now(); + progressState.lastReportAt = progressState.startedAt; + const int progressTotalFrames = maxCallbackOutputFrames(options); + reportEncodingProgress(0, progressTotalFrames, progressState, ProgressCounters{}, true); + CallbackEncodeState callbackState{ + context, + &options, + bitrate, + &sink, + &decodeMs, + &encodeMs, + &encodedFrames, + 0, + webcamCachePtr, + cursorTrackPtr, + zoomTrackPtr, + &progressState, + useDecoderFramePolicy, + 0, + useSourcePts ? &sourcePts : nullptr, + }; + if (options.callbackEncode) { + decoder->SetMappedFrameHandler(encodeMappedDisplayFrame, &callbackState); + } + + auto prepareWebcamFrames = [&]() { + if (!webcamStream) { + return; + } + int outputFrameIndex = encodedFrames + kWebcamPrefetchOutputFrames; + if (options.maxFrames > 0) { + outputFrameIndex = std::min(outputFrameIndex, options.maxFrames - 1); + } + if (options.targetFrames > 0) { + outputFrameIndex = std::min(outputFrameIndex, options.targetFrames - 1); + } + webcamStream->ensureFrame(webcamFrameIndexForOutputFrame(outputFrameIndex, options)); + + const int keepFromOutputFrame = std::max(0, encodedFrames - 8); + webcamStream->dropBefore(webcamFrameIndexForOutputFrame(keepFromOutputFrame, options)); + }; + + while (input && shouldContinueEncoding(encodedFrames, options)) { + prepareWebcamFrames(); + input.read(reinterpret_cast(chunk.data()), static_cast(chunk.size())); + const int bytesRead = static_cast(input.gcount()); + if (bytesRead <= 0) { + break; + } + + const auto decodeStart = std::chrono::steady_clock::now(); + decoder->Decode(chunk.data(), bytesRead, &frames, &returnedFrames); + const auto decodeEnd = std::chrono::steady_clock::now(); + decodeMs += elapsedMs(decodeStart, decodeEnd); + + for (int index = 0; index < returnedFrames; ++index) { + if (!shouldContinueEncoding(encodedFrames, options)) { + break; + } + const int sourceFrameIndex = sourceFrames++; + if (!useDecoderFramePolicy && !shouldEncodeFrame(sourceFrameIndex, encodedFrames, options)) { + continue; + } + if (!sink) { + sink = std::make_unique( + context, + decoder->GetWidth(), + decoder->GetHeight(), + options.fps, + bitrate, + options.outputPath, + options.streamSync, + options, + webcamCachePtr, + cursorTrackPtr, + zoomTrackPtr); + } + const auto encodeStart = std::chrono::steady_clock::now(); + sink->encodeFrame( + frames[index], + decoder->GetDeviceFramePitch(), + decoder->GetWidth(), + decoder->GetHeight(), + decoder->GetHeight(), + encodedFrames); + const auto encodeEnd = std::chrono::steady_clock::now(); + encodeMs += elapsedMs(encodeStart, encodeEnd); + ++encodedFrames; + reportEncodingProgress( + encodedFrames, + progressTotalFrames, + progressState, + collectProgressCounters(sink.get(), webcamCachePtr, decodeMs, encodeMs)); + } + } + + if (shouldContinueEncoding(encodedFrames, options)) { + const auto decodeStart = std::chrono::steady_clock::now(); + decoder->Decode(nullptr, 0, &frames, &returnedFrames); + const auto decodeEnd = std::chrono::steady_clock::now(); + decodeMs += elapsedMs(decodeStart, decodeEnd); + for (int index = 0; index < returnedFrames; ++index) { + if (!shouldContinueEncoding(encodedFrames, options)) { + break; + } + const int sourceFrameIndex = sourceFrames++; + if (!useDecoderFramePolicy && !shouldEncodeFrame(sourceFrameIndex, encodedFrames, options)) { + continue; + } + if (!sink) { + sink = std::make_unique( + context, + decoder->GetWidth(), + decoder->GetHeight(), + options.fps, + bitrate, + options.outputPath, + options.streamSync, + options, + webcamCachePtr, + cursorTrackPtr, + zoomTrackPtr); + } + const auto encodeStart = std::chrono::steady_clock::now(); + sink->encodeFrame( + frames[index], + decoder->GetDeviceFramePitch(), + decoder->GetWidth(), + decoder->GetHeight(), + decoder->GetHeight(), + encodedFrames); + const auto encodeEnd = std::chrono::steady_clock::now(); + encodeMs += elapsedMs(encodeStart, encodeEnd); + ++encodedFrames; + reportEncodingProgress( + encodedFrames, + progressTotalFrames, + progressState, + collectProgressCounters(sink.get(), webcamCachePtr, decodeMs, encodeMs)); + } + } + + if (!sink) { + fail("No decoded frames were produced"); + } + const auto flushStart = std::chrono::steady_clock::now(); + sink->finish(); + const auto flushEnd = std::chrono::steady_clock::now(); + const auto totalEnd = std::chrono::steady_clock::now(); + reportEncodingProgress( + encodedFrames, + progressTotalFrames, + progressState, + collectProgressCounters(sink.get(), webcamCachePtr, decodeMs, encodeMs), + true); + + const double totalMs = elapsedMs(totalStart, totalEnd); + const double mediaMs = static_cast(encodedFrames) * 1000.0 / options.fps; + const double measuredFps = static_cast(encodedFrames) / (totalMs / 1000.0); + const double realtime = mediaMs / totalMs; + const int reportedSourceFrames = + useDecoderFramePolicy ? selectionState.sourceFrames : + (options.callbackEncode ? decoder->GetDisplayFrameCount() : sourceFrames); + const int mappedDisplayFrames = options.callbackEncode ? callbackState.mappedFrames : encodedFrames; + const int selectedDisplayFrames = + useDecoderFramePolicy ? selectionState.selectedFrames : mappedDisplayFrames; + const int skippedDisplayFrames = + useDecoderFramePolicy ? std::max(0, selectionState.sourceFrames - selectionState.selectedFrames) : 0; + const double decodeOnlyApproxMs = options.callbackEncode ? std::max(0.0, decodeMs - encodeMs) : decodeMs; + + std::cout << std::fixed << std::setprecision(2) + << "{" + << "\"success\":true," + << "\"mode\":\"nvdec-cuda-nvenc-annexb\"," + << "\"selectionStage\":\"" + << (options.callbackEncode + ? (useDecoderFramePolicy ? "decoder-policy-mapped-callback" : "mapped-callback") + : (useDecoderFramePolicy ? "decoder" : "post")) + << "\"," + << "\"sourceTimestampMode\":\"" << (useSourcePts ? "pts" : "ordinal") << "\"," + << "\"syncMode\":\"" << (options.streamSync ? "stream" : "device") << "\"," + << "\"prewarmMs\":" << options.prewarmMs << "," + << "\"chunkMb\":" << options.chunkMb << "," + << "\"width\":" << decoder->GetWidth() << "," + << "\"height\":" << decoder->GetHeight() << "," + << "\"fps\":" << options.fps << "," + << "\"staticLayout\":" << (hasStaticLayout(options) ? "true" : "false") << "," + << "\"contentX\":" << options.contentX << "," + << "\"contentY\":" << options.contentY << "," + << "\"contentWidth\":" << options.contentWidth << "," + << "\"contentHeight\":" << options.contentHeight << "," + << "\"radius\":" << options.radius << "," + << "\"backgroundImage\":" << (!options.backgroundNv12Path.empty() ? "true" : "false") << "," + << "\"shadowOffsetY\":" << options.shadowOffsetY << "," + << "\"shadowIntensityPct\":" << options.shadowIntensityPct << "," + << "\"webcamOverlay\":" << (hasWebcamOverlay(options) ? "true" : "false") << "," + << "\"webcamX\":" << options.webcamX << "," + << "\"webcamY\":" << options.webcamY << "," + << "\"webcamSize\":" << options.webcamSize << "," + << "\"webcamRadius\":" << options.webcamRadius << "," + << "\"webcamStream\":" << (!options.webcamAnnexbPath.empty() ? "true" : "false") << "," + << "\"webcamMirror\":" << (options.webcamMirror ? "true" : "false") << "," + << "\"webcamCachedFrames\":" << (webcamCachePtr ? webcamCachePtr->frames.size() : 0) << "," + << "\"webcamPeakCachedFrames\":" << (webcamCachePtr ? webcamCachePtr->peakFrames : 0) << "," + << "\"webcamCacheBaseFrame\":" << (webcamCachePtr ? webcamCachePtr->baseFrameIndex : 0) << "," + << "\"webcamDecodedFrames\":" << (webcamCachePtr ? webcamCachePtr->decodedFrames : 0) << "," + << "\"webcamDecodeMs\":" << (webcamCachePtr ? webcamCachePtr->decodeMs : 0.0) << "," + << "\"webcamCopyMs\":" << (webcamCachePtr ? webcamCachePtr->copyMs : 0.0) << "," + << "\"cursorOverlay\":" << (cursorTrackPtr ? "true" : "false") << "," + << "\"cursorSamples\":" << (cursorTrackPtr ? cursorTrackPtr->samples.size() : 0) << "," + << "\"cursorHeight\":" << options.cursorHeight << "," + << "\"cursorAtlas\":" << (!options.cursorAtlasRgbaPath.empty() ? "true" : "false") << "," + << "\"zoomOverlay\":" << (zoomTrackPtr ? "true" : "false") << "," + << "\"zoomSamples\":" << (zoomTrackPtr ? zoomTrackPtr->samples.size() : 0) << "," + << "\"sourceFrames\":" << reportedSourceFrames << "," + << "\"mappedDisplayFrames\":" << mappedDisplayFrames << "," + << "\"selectedDisplayFrames\":" << selectedDisplayFrames << "," + << "\"skippedDisplayFrames\":" << skippedDisplayFrames << "," + << "\"frames\":" << encodedFrames << "," + << "\"totalMs\":" << totalMs << "," + << "\"decodeMs\":" << decodeOnlyApproxMs << "," + << "\"decodeWallMs\":" << decodeMs << "," + << "\"encodeMs\":" << encodeMs << "," + << "\"compositeMs\":" << sink->compositeMs() << "," + << "\"roiCompositeFrames\":" << sink->roiCompositeFrames() << "," + << "\"monolithicCompositeFrames\":" << sink->monolithicCompositeFrames() << "," + << "\"copyCompositeFrames\":" << sink->copyCompositeFrames() << "," + << "\"nvencMs\":" << sink->nvencMs() << "," + << "\"packetWriteMs\":" << sink->packetWriteMs() << "," + << "\"flushMs\":" << elapsedMs(flushStart, flushEnd) << "," + << "\"measuredFps\":" << measuredFps << "," + << "\"realtimeMultiplier\":" << realtime << "," + << "\"outputBytes\":" << sink->outputBytes() << "," + << "\"outputPath\":\"" << options.outputPath << "\"" + << "}" << std::endl; + + sink.reset(); + decoder.reset(); + webcamStream.reset(); + checkCu(cuCtxDestroy(context), "cuCtxDestroy"); + return 0; + } catch (const std::exception& error) { + std::cerr << "{\"success\":false,\"error\":\"" << error.what() << "\"}" << std::endl; + return 1; + } +} diff --git a/package.json b/package.json index aee7e919..f2dcda9f 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,9 @@ "rebuild:native": "node ./node_modules/@electron/rebuild/lib/cli.js --force --only uiohook-napi", "build:native-helpers": "node scripts/build-native-helpers.mjs", "build:whisper-runtime": "node scripts/build-whisper-runtime.mjs", - "build:platform-native-helpers": "npm run build:native-helpers && npm run build:windows-capture && npm run build:windows-gpu-export && npm run build:cursor-monitor && npm run build:whisper-runtime", + "build:platform-native-helpers": "npm run build:native-helpers && npm run build:windows-capture && npm run build:windows-gpu-export && npm run build:nvidia-cuda-compositor && npm run build:cursor-monitor && npm run build:whisper-runtime", "build:windows-gpu-export": "node scripts/build-windows-gpu-export.mjs", + "build:nvidia-cuda-compositor": "node scripts/build-nvidia-cuda-compositor.mjs", "build:windows-capture": "node scripts/build-windows-capture.mjs", "build:cursor-monitor": "node scripts/build-cursor-monitor.mjs", "build:mac": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && electron-builder --mac", diff --git a/scripts/build-nvidia-cuda-compositor.mjs b/scripts/build-nvidia-cuda-compositor.mjs new file mode 100644 index 00000000..cdccbc12 --- /dev/null +++ b/scripts/build-nvidia-cuda-compositor.mjs @@ -0,0 +1,193 @@ +import { execSync } from "node:child_process"; +import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs"; +import path from "node:path"; + +import { + formatNativeHelperManifestWarning, + updateNativeHelperManifest, + verifyNativeHelperManifest, +} from "./native-helper-manifest.mjs"; + +const projectRoot = process.cwd(); +const sourceDir = path.join(projectRoot, "electron", "native", "nvidia-cuda-compositor"); +const buildDir = path.join(sourceDir, "build"); +const bundledDir = path.join( + projectRoot, + "electron", + "native", + "bin", + process.arch === "arm64" ? "win32-arm64" : "win32-x64", +); +const bundledExePath = path.join(bundledDir, "recordly-nvidia-cuda-compositor.exe"); +const helperId = "recordly-nvidia-cuda-compositor"; +const generatorArch = process.arch === "arm64" ? "ARM64" : "x64"; +const videoCodecSdkRoot = + process.env.RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT?.trim() || + path.join(projectRoot, ".tmp", "video-sdk-samples"); + +if (process.platform !== "win32") { + console.log("[build-nvidia-cuda-compositor] Skipping NVIDIA CUDA compositor build."); + process.exit(0); +} + +if (!existsSync(path.join(sourceDir, "CMakeLists.txt"))) { + console.error("[build-nvidia-cuda-compositor] CMakeLists.txt not found at", sourceDir); + process.exit(1); +} + +function fallbackToBundledHelperOrExit(reason) { + if (existsSync(bundledExePath)) { + const verification = verifyNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "recordly-nvidia-cuda-compositor.exe", + }); + if (!verification.ok) { + console.warn( + formatNativeHelperManifestWarning("build-nvidia-cuda-compositor", verification), + ); + } + console.log(`[build-nvidia-cuda-compositor] ${reason}`); + console.log(`[build-nvidia-cuda-compositor] Using bundled helper: ${bundledExePath}`); + process.exit(0); + } + + console.error(`[build-nvidia-cuda-compositor] ${reason}`); + console.error( + "[build-nvidia-cuda-compositor] No bundled helper is available; install CUDA Toolkit + NVIDIA Video Codec SDK or provide a staged helper.", + ); + process.exit(1); +} + +function findCmake() { + try { + execSync("cmake --version", { stdio: "pipe" }); + return "cmake"; + } catch { + // Continue probing common Windows install locations. + } + + const standaloneCmakePaths = [ + path.join("C:", "Program Files", "CMake", "bin", "cmake.exe"), + path.join("C:", "Program Files (x86)", "CMake", "bin", "cmake.exe"), + ]; + for (const cmakePath of standaloneCmakePaths) { + if (existsSync(cmakePath)) { + return `"${cmakePath}"`; + } + } + + const vsRoots = [ + path.join("C:", "Program Files", "Microsoft Visual Studio"), + path.join("C:", "Program Files (x86)", "Microsoft Visual Studio"), + ]; + const vsEditions = ["Preview", "Community", "Professional", "Enterprise", "BuildTools"]; + const vsVersions = ["2022", "2019"]; + for (const root of vsRoots) { + for (const version of vsVersions) { + for (const edition of vsEditions) { + const cmakePath = path.join( + root, + version, + edition, + "Common7", + "IDE", + "CommonExtensions", + "Microsoft", + "CMake", + "CMake", + "bin", + "cmake.exe", + ); + if (existsSync(cmakePath)) { + return `"${cmakePath}"`; + } + } + } + } + + return null; +} + +if (!existsSync(path.join(videoCodecSdkRoot, "Samples", "NvCodec"))) { + fallbackToBundledHelperOrExit( + `NVIDIA Video Codec SDK samples not found at ${videoCodecSdkRoot}. Set RECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT to build from source.`, + ); +} + +const cmake = findCmake(); +if (!cmake) { + fallbackToBundledHelperOrExit( + "CMake not found. Install Visual Studio with C++ CMake tools or standalone CMake.", + ); +} + +mkdirSync(buildDir, { recursive: true }); + +function clearCmakeCache() { + rmSync(path.join(buildDir, "CMakeCache.txt"), { force: true }); + rmSync(path.join(buildDir, "CMakeFiles"), { recursive: true, force: true }); +} + +console.log("[build-nvidia-cuda-compositor] Configuring CMake..."); +try { + clearCmakeCache(); + execSync( + `${cmake} .. -G "Visual Studio 17 2022" -A ${generatorArch} -DRECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT="${videoCodecSdkRoot}"`, + { + cwd: buildDir, + stdio: "inherit", + timeout: 120000, + }, + ); +} catch { + console.log("[build-nvidia-cuda-compositor] VS 2022 generator not found, trying VS 2019..."); + try { + clearCmakeCache(); + execSync( + `${cmake} .. -G "Visual Studio 16 2019" -A ${generatorArch} -DRECORDLY_NVIDIA_VIDEO_CODEC_SDK_ROOT="${videoCodecSdkRoot}"`, + { + cwd: buildDir, + stdio: "inherit", + timeout: 120000, + }, + ); + } catch (error) { + fallbackToBundledHelperOrExit( + `CMake configure failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} + +console.log("[build-nvidia-cuda-compositor] Building NVIDIA CUDA compositor..."); +try { + execSync(`${cmake} --build . --config Release`, { + cwd: buildDir, + stdio: "inherit", + timeout: 300000, + }); +} catch (error) { + fallbackToBundledHelperOrExit( + `Build failed: ${error instanceof Error ? error.message : String(error)}`, + ); +} + +const exePath = path.join(buildDir, "Release", "recordly-nvidia-cuda-compositor.exe"); +if (!existsSync(exePath)) { + console.error("[build-nvidia-cuda-compositor] Expected exe not found at", exePath); + process.exit(1); +} + +mkdirSync(bundledDir, { recursive: true }); +copyFileSync(exePath, bundledExePath); +console.log(`[build-nvidia-cuda-compositor] Staged bundled helper: ${bundledExePath}`); +const manifestPath = updateNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "recordly-nvidia-cuda-compositor.exe", +}); +console.log(`[build-nvidia-cuda-compositor] Updated helper manifest: ${manifestPath}`); diff --git a/scripts/launch-recordly-cuda-auto.ps1 b/scripts/launch-recordly-cuda-auto.ps1 new file mode 100644 index 00000000..02d3dff2 --- /dev/null +++ b/scripts/launch-recordly-cuda-auto.ps1 @@ -0,0 +1,80 @@ +param( + [string]$AppPath, + [string]$CudaScriptPath, + [switch]$CloseExisting, + [switch]$NoDiagnostics, + [switch]$AllowCudaAudio +) + +$ErrorActionPreference = "Stop" + +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") + +if (-not $AppPath) { + $packagedAppPath = Join-Path $repoRoot "release\win-unpacked\Recordly.exe" + $installedAppPath = Join-Path $env:LOCALAPPDATA "Programs\recordly\Recordly.exe" + if (Test-Path $packagedAppPath) { + $AppPath = $packagedAppPath + } elseif (Test-Path $installedAppPath) { + $AppPath = $installedAppPath + } else { + throw "Recordly.exe was not found. Build the Windows package first or pass -AppPath." + } +} + +if (-not $CudaScriptPath) { + $CudaScriptPath = Join-Path $repoRoot "electron\native\nvidia-cuda-compositor\run-mp4-pipeline.mjs" +} + +if (-not (Test-Path $AppPath)) { + throw "Recordly app not found: $AppPath" +} + +if (-not (Test-Path $CudaScriptPath)) { + throw "NVIDIA CUDA/NVENC wrapper script not found: $CudaScriptPath" +} + +$existingRecordly = @(Get-Process -Name "Recordly" -ErrorAction SilentlyContinue) +if ($existingRecordly.Count -gt 0) { + if (-not $CloseExisting) { + Write-Host "Recordly is already running. Close it first, or rerun with -CloseExisting so the CUDA env is inherited by the new app process." + $existingRecordly | Select-Object Id, ProcessName, Path | Format-Table -AutoSize + exit 2 + } + + $existingRecordly | Stop-Process -Force + Start-Sleep -Milliseconds 500 +} + +$env:RECORDLY_EXPERIMENTAL_NVIDIA_CUDA_EXPORT = "1" +$env:RECORDLY_NVIDIA_CUDA_EXPORT_SCRIPT = (Resolve-Path $CudaScriptPath).Path +$env:RECORDLY_NVIDIA_CUDA_EXPORT_HIGH_PRIORITY = "1" +$env:RECORDLY_NVIDIA_CUDA_SAMPLE_GPU = "1" + +if ($AllowCudaAudio) { + $env:RECORDLY_NVIDIA_CUDA_ALLOW_AUDIO_EXPORT = "1" +} else { + Remove-Item Env:\RECORDLY_NVIDIA_CUDA_ALLOW_AUDIO_EXPORT -ErrorAction SilentlyContinue +} + +if ($NoDiagnostics) { + Remove-Item Env:\RECORDLY_NVIDIA_CUDA_EXPORT_DIAGNOSTICS -ErrorAction SilentlyContinue +} else { + $env:RECORDLY_NVIDIA_CUDA_EXPORT_DIAGNOSTICS = "1" +} + +$resolvedAppPath = (Resolve-Path $AppPath).Path +$appDirectory = Split-Path $resolvedAppPath -Parent +if ($AllowCudaAudio) { + $cudaAudioMode = "candidate only; app still requires timestamp-aligned CUDA output" +} else { + $cudaAudioMode = "guarded; audio exports fall back to Windows D3D11" +} + +Write-Host "Launching Recordly with guarded NVIDIA CUDA/NVENC auto export enabled:" +Write-Host " App: $resolvedAppPath" +Write-Host " CUDA wrapper: $env:RECORDLY_NVIDIA_CUDA_EXPORT_SCRIPT" +Write-Host " CUDA audio exports: $cudaAudioMode" +Write-Host " Diagnostics: $($env:RECORDLY_NVIDIA_CUDA_EXPORT_DIAGNOSTICS -eq '1')" + +Start-Process -FilePath $resolvedAppPath -WorkingDirectory $appDirectory diff --git a/scripts/smoke-packaged-binaries.mjs b/scripts/smoke-packaged-binaries.mjs index 745f787b..97e5eb65 100644 --- a/scripts/smoke-packaged-binaries.mjs +++ b/scripts/smoke-packaged-binaries.mjs @@ -171,6 +171,11 @@ function getExpectedNativeHelperFiles(archTag) { label: "Windows GPU export helper", executable: true, }, + { + name: "recordly-nvidia-cuda-compositor.exe", + label: "NVIDIA CUDA compositor helper", + executable: true, + }, { name: "helpers-manifest.json", label: "Windows helper manifest" }, { name: "whisper-cli.exe", label: "Whisper CLI runtime", executable: true }, { name: "whisper-runtime.json", label: "Whisper runtime manifest" }, diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 1de21039..0f12fca3 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -731,16 +731,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { await window.electronAPI.muxNativeWindowsRecording(pauseSegments); if (!muxResult?.success || !muxResult.path) { void logNativeCaptureDiagnostics("mux-native-windows-recording"); - if (!muxResult?.path) { - const failureMessage = await buildNativeCaptureFailureMessage( - "mux-native-windows-recording", - muxResult?.message || - "Failed to finalize the Windows recording, so the editor was not opened.", - ); - await notifyRecordingFinalizationFailure(failureMessage); - return; - } - + const fallbackPath = muxResult?.path ?? finalPath; const warningMessage = muxResult?.error || muxResult?.message || @@ -749,8 +740,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { `${warningMessage}. Recording was saved, but audio playback or export may be incomplete.`, { id: SOURCE_AUDIO_MUX_TOAST_ID, duration: 10000 }, ); + finalPath = fallbackPath; + } else { + finalPath = muxResult.path; } - finalPath = muxResult.path; } await storeMicrophoneSidecar( @@ -1186,7 +1179,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { maxFrameRate: TARGET_FRAME_RATE, }, }, - } as any); + } as unknown as MediaStreamConstraints); } }; diff --git a/src/lib/exporter/modernVideoExporter.ts b/src/lib/exporter/modernVideoExporter.ts index 17a2f99b..bb374f12 100644 --- a/src/lib/exporter/modernVideoExporter.ts +++ b/src/lib/exporter/modernVideoExporter.ts @@ -1601,8 +1601,14 @@ export class ModernVideoExporter { return; } - const nativeCurrentFrame = Math.floor(progress.currentFrame); const nativeTotalFrames = Math.max(1, Math.floor(progress.totalFrames)); + const progressPercentFrame = Number.isFinite(progress.percentage) + ? Math.floor((nativeTotalFrames * progress.percentage) / 100) + : 0; + const nativeCurrentFrame = Math.max( + Math.floor(progress.currentFrame), + progressPercentFrame, + ); const nativeFramesComplete = nativeCurrentFrame >= nativeTotalFrames; const maxExtractingFrame = Math.max( 0,