feat(encode/nvenc): SPIR-V cursor blend over Vulkan-allocated input slots — retire the PTX kernels
A vendored PTX blob is JIT'd against the driver's ISA ceiling, so the cursor-blend module silently dies on drivers older than the generating toolkit (CUDA_ERROR_UNSUPPORTED_PTX_VERSION/INVALID_PTX, 222/218 — the KWin leg's invisible composite cursor on driver 595/CUDA 13.2 vs a CUDA 13.3 blob). SPIR-V has no such coupling, and the in-tree precedent already exists twice (vulkan_video's CSC blend, VkBridge's exportable OPAQUE_FD → cuImportExternalMemory bridge). New pf_zerocopy::vkslot::VkSlotBlend: the direct-SDK NVENC encoder now allocates its input ring as exportable Vulkan buffers CUDA-imports (same contiguous InputSurface layouts, pitch = row bytes rounded to 256), and the cursor composite is a compute dispatch over the cursor's rectangle (cursor_blend.comp, vendored .spv; spec-constant selects ARGB/NV12/YUV444; BT.709 limited, matching the retired .cu). The surface SSBO is uint[] with every invocation owning whole words — no 8-bit-storage device dependency. Cursor-bearing frames force the existing CPU-synced submit path so the CUDA copy → Vulkan dispatch (fence-waited) → NVENC encode ordering is CPU-established; cursorless frames keep the stream-ordered fast path untouched. Any bring-up/alloc/registration failure falls back wholesale to plain pitched CUDA surfaces (never a mixed or short ring): sessions always encode, composite mode just loses the cursor, warned once. cursor_blend.cu / cursor_blend.ptx and the CursorBlend PTX loader are deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,105 +0,0 @@
|
|||||||
// Cursor-overlay blend kernels for the CUDA/NVENC path (cursor-as-metadata). The cursor bitmap is
|
|
||||||
// straight-alpha RGBA, row-packed (stride = curW*4). Blended into the encoder-OWNED NVENC input
|
|
||||||
// surface — never the compositor's dmabuf. One thread per cursor pixel (ARGB / YUV444) or per 2x2
|
|
||||||
// chroma block (NV12). Coefficients are BT.709 limited, matching rgb2yuv.comp so the cursor colour
|
|
||||||
// matches the rest of the frame regardless of which zero-copy backend encodes it.
|
|
||||||
//
|
|
||||||
// Build (regenerate cursor_blend.ptx after editing):
|
|
||||||
// nvcc -ptx -arch=compute_75 cursor_blend.cu -o cursor_blend.ptx
|
|
||||||
// PTX is JIT'd by the driver forward to the actual GPU, so a compute_75 (Turing) baseline runs on
|
|
||||||
// every Turing-or-newer NVENC GPU. (CUDA 13's nvcc no longer targets pre-Turing archs.)
|
|
||||||
|
|
||||||
typedef unsigned char u8;
|
|
||||||
|
|
||||||
__device__ __forceinline__ u8 blend8(int dst, int src, int a) {
|
|
||||||
return (u8)((src * a + dst * (255 - a)) / 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Packed 4-byte surface. NVENC's ARGB format stores bytes B,G,R,A in memory; the cursor is R,G,B,A.
|
|
||||||
extern "C" __global__ void blend_argb(
|
|
||||||
u8* surf, int pitch, int surfW, int surfH,
|
|
||||||
const u8* cur, int curW, int curH, int ox, int oy)
|
|
||||||
{
|
|
||||||
int cx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
||||||
int cy = blockIdx.y * blockDim.y + threadIdx.y;
|
|
||||||
if (cx >= curW || cy >= curH) return;
|
|
||||||
int px = ox + cx, py = oy + cy;
|
|
||||||
if (px < 0 || py < 0 || px >= surfW || py >= surfH) return;
|
|
||||||
const u8* s = cur + (size_t)(cy * curW + cx) * 4;
|
|
||||||
int a = s[3];
|
|
||||||
if (a == 0) return;
|
|
||||||
u8* d = surf + (size_t)py * pitch + (size_t)px * 4;
|
|
||||||
d[0] = blend8(d[0], s[2], a); // B <- cursor B
|
|
||||||
d[1] = blend8(d[1], s[1], a); // G <- cursor G
|
|
||||||
d[2] = blend8(d[2], s[0], a); // R <- cursor R
|
|
||||||
}
|
|
||||||
|
|
||||||
// Planar YUV444: three full-res planes stacked at base, base+plane, base+2*plane (plane=pitch*surfH).
|
|
||||||
extern "C" __global__ void blend_yuv444(
|
|
||||||
u8* base, int pitch, int surfW, int surfH,
|
|
||||||
const u8* cur, int curW, int curH, int ox, int oy)
|
|
||||||
{
|
|
||||||
int cx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
||||||
int cy = blockIdx.y * blockDim.y + threadIdx.y;
|
|
||||||
if (cx >= curW || cy >= curH) return;
|
|
||||||
int px = ox + cx, py = oy + cy;
|
|
||||||
if (px < 0 || py < 0 || px >= surfW || py >= surfH) return;
|
|
||||||
const u8* s = cur + (size_t)(cy * curW + cx) * 4;
|
|
||||||
int a = s[3];
|
|
||||||
if (a == 0) return;
|
|
||||||
float R = s[0], G = s[1], B = s[2];
|
|
||||||
int Y = (int)(16.0f + 0.1826f * R + 0.6142f * G + 0.0620f * B + 0.5f);
|
|
||||||
int U = (int)(128.0f - 0.1006f * R - 0.3386f * G + 0.4392f * B + 0.5f);
|
|
||||||
int V = (int)(128.0f + 0.4392f * R - 0.3989f * G - 0.0403f * B + 0.5f);
|
|
||||||
size_t plane = (size_t)pitch * surfH;
|
|
||||||
u8* yp = base + (size_t)py * pitch + px;
|
|
||||||
u8* up = base + plane + (size_t)py * pitch + px;
|
|
||||||
u8* vp = base + 2 * plane + (size_t)py * pitch + px;
|
|
||||||
*yp = blend8(*yp, Y, a);
|
|
||||||
*up = blend8(*up, U, a);
|
|
||||||
*vp = blend8(*vp, V, a);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NV12: full-res Y plane + interleaved half-res UV plane. One thread per 2x2 luma block; each blends
|
|
||||||
// up to four Y samples and one (alpha-weighted) UV sample.
|
|
||||||
extern "C" __global__ void blend_nv12(
|
|
||||||
u8* yb, int yPitch, u8* uvb, int uvPitch, int surfW, int surfH,
|
|
||||||
const u8* cur, int curW, int curH, int ox, int oy)
|
|
||||||
{
|
|
||||||
int bx = blockIdx.x * blockDim.x + threadIdx.x;
|
|
||||||
int by = blockIdx.y * blockDim.y + threadIdx.y;
|
|
||||||
int base_cx = bx * 2, base_cy = by * 2;
|
|
||||||
if (base_cx >= curW || base_cy >= curH) return;
|
|
||||||
float ua = 0.0f, va = 0.0f, wa = 0.0f;
|
|
||||||
int cnt = 0;
|
|
||||||
for (int j = 0; j < 2; j++) {
|
|
||||||
for (int i = 0; i < 2; i++) {
|
|
||||||
int cx = base_cx + i, cy = base_cy + j;
|
|
||||||
if (cx >= curW || cy >= curH) continue;
|
|
||||||
int px = ox + cx, py = oy + cy;
|
|
||||||
if (px < 0 || py < 0 || px >= surfW || py >= surfH) continue;
|
|
||||||
const u8* s = cur + (size_t)(cy * curW + cx) * 4;
|
|
||||||
int a = s[3];
|
|
||||||
if (a == 0) continue;
|
|
||||||
float R = s[0], G = s[1], B = s[2];
|
|
||||||
int Y = (int)(16.0f + 0.1826f * R + 0.6142f * G + 0.0620f * B + 0.5f);
|
|
||||||
u8* yp = yb + (size_t)py * yPitch + px;
|
|
||||||
*yp = blend8(*yp, Y, a);
|
|
||||||
ua += (128.0f - 0.1006f * R - 0.3386f * G + 0.4392f * B) * a;
|
|
||||||
va += (128.0f + 0.4392f * R - 0.3989f * G - 0.0403f * B) * a;
|
|
||||||
wa += a;
|
|
||||||
cnt++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (wa <= 0.0f || cnt == 0) return;
|
|
||||||
// The chroma sample covering this block's top-left surface pixel.
|
|
||||||
int uvx = (ox + base_cx) / 2;
|
|
||||||
int uvy = (oy + base_cy) / 2;
|
|
||||||
if (uvx < 0 || uvy < 0 || uvx * 2 >= surfW || uvy * 2 >= surfH) return;
|
|
||||||
int U = (int)(ua / wa + 0.5f);
|
|
||||||
int V = (int)(va / wa + 0.5f);
|
|
||||||
int amean = (int)(wa / cnt + 0.5f);
|
|
||||||
u8* uv = uvb + (size_t)uvy * uvPitch + (size_t)uvx * 2;
|
|
||||||
uv[0] = blend8(uv[0], U, amean);
|
|
||||||
uv[1] = blend8(uv[1], V, amean);
|
|
||||||
}
|
|
||||||
@@ -1,584 +0,0 @@
|
|||||||
//
|
|
||||||
// Generated by NVIDIA NVVM Compiler
|
|
||||||
//
|
|
||||||
// Compiler Build ID: CL-38244171
|
|
||||||
// Cuda compilation tools, release 13.3, V13.3.73
|
|
||||||
// Based on NVVM 7.0.1
|
|
||||||
//
|
|
||||||
|
|
||||||
// .version lowered from the generating toolkit's 9.3 (CUDA 13.3) by hand: a driver refuses PTX
|
|
||||||
// whose ISA version is newer than its JIT (CUDA_ERROR_UNSUPPORTED_PTX_VERSION, 222 — driver
|
|
||||||
// 595.58 = CUDA 13.2 rejects a 9.3 stamp), while a stamp OLDER than the body's actual syntax is
|
|
||||||
// rejected as CUDA_ERROR_INVALID_PTX (218 — 8.0 was tried and failed against this body). 9.2 is
|
|
||||||
// the highest ISA a CUDA-13.2 driver JITs and the lowest this 13.3-emitted body accepts.
|
|
||||||
// TODO(portability): drivers older than the 13.2 era will refuse 9.2 too — the real fix is
|
|
||||||
// regenerating cursor_blend.cu with an older toolkit (CUDA 12.x) so the body itself is old, then
|
|
||||||
// stamping that toolkit's native version. Re-test on the oldest driver box after any regen.
|
|
||||||
.version 9.2
|
|
||||||
.target sm_75
|
|
||||||
.address_size 64
|
|
||||||
|
|
||||||
// .globl blend_argb
|
|
||||||
|
|
||||||
.visible .entry blend_argb(
|
|
||||||
.param .u64 blend_argb_param_0,
|
|
||||||
.param .u32 blend_argb_param_1,
|
|
||||||
.param .u32 blend_argb_param_2,
|
|
||||||
.param .u32 blend_argb_param_3,
|
|
||||||
.param .u64 blend_argb_param_4,
|
|
||||||
.param .u32 blend_argb_param_5,
|
|
||||||
.param .u32 blend_argb_param_6,
|
|
||||||
.param .u32 blend_argb_param_7,
|
|
||||||
.param .u32 blend_argb_param_8
|
|
||||||
)
|
|
||||||
{
|
|
||||||
.reg .pred %p<10>;
|
|
||||||
.reg .b16 %rs<2>;
|
|
||||||
.reg .b32 %r<34>;
|
|
||||||
.reg .b64 %rd<17>;
|
|
||||||
|
|
||||||
|
|
||||||
ld.param.u64 %rd2, [blend_argb_param_0];
|
|
||||||
ld.param.u32 %r5, [blend_argb_param_1];
|
|
||||||
ld.param.u32 %r6, [blend_argb_param_2];
|
|
||||||
ld.param.u32 %r7, [blend_argb_param_3];
|
|
||||||
ld.param.u64 %rd3, [blend_argb_param_4];
|
|
||||||
ld.param.u32 %r8, [blend_argb_param_5];
|
|
||||||
ld.param.u32 %r11, [blend_argb_param_6];
|
|
||||||
ld.param.u32 %r9, [blend_argb_param_7];
|
|
||||||
ld.param.u32 %r10, [blend_argb_param_8];
|
|
||||||
mov.u32 %r12, %ntid.x;
|
|
||||||
mov.u32 %r13, %ctaid.x;
|
|
||||||
mov.u32 %r14, %tid.x;
|
|
||||||
mad.lo.s32 %r1, %r13, %r12, %r14;
|
|
||||||
mov.u32 %r15, %ntid.y;
|
|
||||||
mov.u32 %r16, %ctaid.y;
|
|
||||||
mov.u32 %r17, %tid.y;
|
|
||||||
mad.lo.s32 %r2, %r16, %r15, %r17;
|
|
||||||
setp.ge.s32 %p1, %r1, %r8;
|
|
||||||
setp.ge.s32 %p2, %r2, %r11;
|
|
||||||
or.pred %p3, %p1, %p2;
|
|
||||||
@%p3 bra $L__BB0_4;
|
|
||||||
|
|
||||||
add.s32 %r3, %r1, %r9;
|
|
||||||
add.s32 %r4, %r2, %r10;
|
|
||||||
or.b32 %r18, %r4, %r3;
|
|
||||||
setp.lt.s32 %p4, %r18, 0;
|
|
||||||
setp.ge.s32 %p5, %r3, %r6;
|
|
||||||
or.pred %p6, %p5, %p4;
|
|
||||||
setp.ge.s32 %p7, %r4, %r7;
|
|
||||||
or.pred %p8, %p7, %p6;
|
|
||||||
@%p8 bra $L__BB0_4;
|
|
||||||
|
|
||||||
mad.lo.s32 %r19, %r2, %r8, %r1;
|
|
||||||
mul.wide.s32 %rd4, %r19, 4;
|
|
||||||
cvta.to.global.u64 %rd5, %rd3;
|
|
||||||
add.s64 %rd1, %rd5, %rd4;
|
|
||||||
ld.global.u8 %rs1, [%rd1+3];
|
|
||||||
setp.eq.s16 %p9, %rs1, 0;
|
|
||||||
@%p9 bra $L__BB0_4;
|
|
||||||
|
|
||||||
cvt.u32.u16 %r20, %rs1;
|
|
||||||
mul.wide.s32 %rd6, %r4, %r5;
|
|
||||||
mul.wide.s32 %rd7, %r3, 4;
|
|
||||||
add.s64 %rd8, %rd6, %rd7;
|
|
||||||
cvta.to.global.u64 %rd9, %rd2;
|
|
||||||
add.s64 %rd10, %rd9, %rd8;
|
|
||||||
ld.global.u8 %r21, [%rd10];
|
|
||||||
ld.global.u8 %r22, [%rd1+2];
|
|
||||||
xor.b32 %r23, %r20, 255;
|
|
||||||
mul.lo.s32 %r24, %r23, %r21;
|
|
||||||
mad.lo.s32 %r25, %r22, %r20, %r24;
|
|
||||||
mul.wide.u32 %rd11, %r25, -2139062143;
|
|
||||||
shr.u64 %rd12, %rd11, 39;
|
|
||||||
st.global.u8 [%rd10], %rd12;
|
|
||||||
ld.global.u8 %r26, [%rd10+1];
|
|
||||||
ld.global.u8 %r27, [%rd1+1];
|
|
||||||
mul.lo.s32 %r28, %r23, %r26;
|
|
||||||
mad.lo.s32 %r29, %r27, %r20, %r28;
|
|
||||||
mul.wide.u32 %rd13, %r29, -2139062143;
|
|
||||||
shr.u64 %rd14, %rd13, 39;
|
|
||||||
st.global.u8 [%rd10+1], %rd14;
|
|
||||||
ld.global.u8 %r30, [%rd10+2];
|
|
||||||
ld.global.u8 %r31, [%rd1];
|
|
||||||
mul.lo.s32 %r32, %r23, %r30;
|
|
||||||
mad.lo.s32 %r33, %r31, %r20, %r32;
|
|
||||||
mul.wide.u32 %rd15, %r33, -2139062143;
|
|
||||||
shr.u64 %rd16, %rd15, 39;
|
|
||||||
st.global.u8 [%rd10+2], %rd16;
|
|
||||||
|
|
||||||
$L__BB0_4:
|
|
||||||
ret;
|
|
||||||
|
|
||||||
}
|
|
||||||
// .globl blend_yuv444
|
|
||||||
.visible .entry blend_yuv444(
|
|
||||||
.param .u64 blend_yuv444_param_0,
|
|
||||||
.param .u32 blend_yuv444_param_1,
|
|
||||||
.param .u32 blend_yuv444_param_2,
|
|
||||||
.param .u32 blend_yuv444_param_3,
|
|
||||||
.param .u64 blend_yuv444_param_4,
|
|
||||||
.param .u32 blend_yuv444_param_5,
|
|
||||||
.param .u32 blend_yuv444_param_6,
|
|
||||||
.param .u32 blend_yuv444_param_7,
|
|
||||||
.param .u32 blend_yuv444_param_8
|
|
||||||
)
|
|
||||||
{
|
|
||||||
.reg .pred %p<10>;
|
|
||||||
.reg .b16 %rs<5>;
|
|
||||||
.reg .f32 %f<16>;
|
|
||||||
.reg .b32 %r<49>;
|
|
||||||
.reg .b64 %rd<14>;
|
|
||||||
|
|
||||||
|
|
||||||
ld.param.u64 %rd2, [blend_yuv444_param_0];
|
|
||||||
ld.param.u32 %r5, [blend_yuv444_param_1];
|
|
||||||
ld.param.u32 %r6, [blend_yuv444_param_2];
|
|
||||||
ld.param.u32 %r7, [blend_yuv444_param_3];
|
|
||||||
ld.param.u64 %rd3, [blend_yuv444_param_4];
|
|
||||||
ld.param.u32 %r8, [blend_yuv444_param_5];
|
|
||||||
ld.param.u32 %r11, [blend_yuv444_param_6];
|
|
||||||
ld.param.u32 %r9, [blend_yuv444_param_7];
|
|
||||||
ld.param.u32 %r10, [blend_yuv444_param_8];
|
|
||||||
mov.u32 %r12, %ntid.x;
|
|
||||||
mov.u32 %r13, %ctaid.x;
|
|
||||||
mov.u32 %r14, %tid.x;
|
|
||||||
mad.lo.s32 %r1, %r13, %r12, %r14;
|
|
||||||
mov.u32 %r15, %ntid.y;
|
|
||||||
mov.u32 %r16, %ctaid.y;
|
|
||||||
mov.u32 %r17, %tid.y;
|
|
||||||
mad.lo.s32 %r2, %r16, %r15, %r17;
|
|
||||||
setp.ge.s32 %p1, %r1, %r8;
|
|
||||||
setp.ge.s32 %p2, %r2, %r11;
|
|
||||||
or.pred %p3, %p1, %p2;
|
|
||||||
@%p3 bra $L__BB1_4;
|
|
||||||
|
|
||||||
add.s32 %r3, %r1, %r9;
|
|
||||||
add.s32 %r4, %r2, %r10;
|
|
||||||
or.b32 %r18, %r4, %r3;
|
|
||||||
setp.lt.s32 %p4, %r18, 0;
|
|
||||||
setp.ge.s32 %p5, %r3, %r6;
|
|
||||||
or.pred %p6, %p5, %p4;
|
|
||||||
setp.ge.s32 %p7, %r4, %r7;
|
|
||||||
or.pred %p8, %p7, %p6;
|
|
||||||
@%p8 bra $L__BB1_4;
|
|
||||||
|
|
||||||
mad.lo.s32 %r19, %r2, %r8, %r1;
|
|
||||||
mul.wide.s32 %rd4, %r19, 4;
|
|
||||||
cvta.to.global.u64 %rd5, %rd3;
|
|
||||||
add.s64 %rd1, %rd5, %rd4;
|
|
||||||
ld.global.u8 %rs1, [%rd1+3];
|
|
||||||
setp.eq.s16 %p9, %rs1, 0;
|
|
||||||
@%p9 bra $L__BB1_4;
|
|
||||||
|
|
||||||
cvt.u32.u16 %r20, %rs1;
|
|
||||||
ld.global.u8 %rs2, [%rd1];
|
|
||||||
cvt.rn.f32.u16 %f1, %rs2;
|
|
||||||
ld.global.u8 %rs3, [%rd1+1];
|
|
||||||
cvt.rn.f32.u16 %f2, %rs3;
|
|
||||||
ld.global.u8 %rs4, [%rd1+2];
|
|
||||||
cvt.rn.f32.u16 %f3, %rs4;
|
|
||||||
fma.rn.f32 %f4, %f1, 0f3E3AFB7F, 0f41800000;
|
|
||||||
fma.rn.f32 %f5, %f2, 0f3F1D3C36, %f4;
|
|
||||||
fma.rn.f32 %f6, %f3, 0f3D7DF3B6, %f5;
|
|
||||||
add.f32 %f7, %f6, 0f3F000000;
|
|
||||||
cvt.rzi.s32.f32 %r21, %f7;
|
|
||||||
fma.rn.f32 %f8, %f1, 0fBDCE075F, 0f43000000;
|
|
||||||
fma.rn.f32 %f9, %f2, 0fBEAD5CFB, %f8;
|
|
||||||
fma.rn.f32 %f10, %f3, 0f3EE0DED3, %f9;
|
|
||||||
add.f32 %f11, %f10, 0f3F000000;
|
|
||||||
cvt.rzi.s32.f32 %r22, %f11;
|
|
||||||
fma.rn.f32 %f12, %f1, 0f3EE0DED3, 0f43000000;
|
|
||||||
fma.rn.f32 %f13, %f2, 0fBECC3C9F, %f12;
|
|
||||||
fma.rn.f32 %f14, %f3, 0fBD25119D, %f13;
|
|
||||||
add.f32 %f15, %f14, 0f3F000000;
|
|
||||||
cvt.rzi.s32.f32 %r23, %f15;
|
|
||||||
mul.wide.s32 %rd6, %r4, %r5;
|
|
||||||
cvt.s64.s32 %rd7, %r3;
|
|
||||||
add.s64 %rd8, %rd6, %rd7;
|
|
||||||
cvta.to.global.u64 %rd9, %rd2;
|
|
||||||
add.s64 %rd10, %rd9, %rd8;
|
|
||||||
ld.global.u8 %r24, [%rd10];
|
|
||||||
mul.lo.s32 %r25, %r21, %r20;
|
|
||||||
xor.b32 %r26, %r20, 255;
|
|
||||||
mad.lo.s32 %r27, %r26, %r24, %r25;
|
|
||||||
mul.hi.s32 %r28, %r27, -2139062143;
|
|
||||||
add.s32 %r29, %r28, %r27;
|
|
||||||
shr.u32 %r30, %r29, 31;
|
|
||||||
shr.u32 %r31, %r29, 7;
|
|
||||||
add.s32 %r32, %r31, %r30;
|
|
||||||
st.global.u8 [%rd10], %r32;
|
|
||||||
mul.wide.s32 %rd11, %r7, %r5;
|
|
||||||
add.s64 %rd12, %rd10, %rd11;
|
|
||||||
ld.global.u8 %r33, [%rd12];
|
|
||||||
mul.lo.s32 %r34, %r22, %r20;
|
|
||||||
mad.lo.s32 %r35, %r26, %r33, %r34;
|
|
||||||
mul.hi.s32 %r36, %r35, -2139062143;
|
|
||||||
add.s32 %r37, %r36, %r35;
|
|
||||||
shr.u32 %r38, %r37, 31;
|
|
||||||
shr.u32 %r39, %r37, 7;
|
|
||||||
add.s32 %r40, %r39, %r38;
|
|
||||||
st.global.u8 [%rd12], %r40;
|
|
||||||
add.s64 %rd13, %rd12, %rd11;
|
|
||||||
ld.global.u8 %r41, [%rd13];
|
|
||||||
mul.lo.s32 %r42, %r23, %r20;
|
|
||||||
mad.lo.s32 %r43, %r26, %r41, %r42;
|
|
||||||
mul.hi.s32 %r44, %r43, -2139062143;
|
|
||||||
add.s32 %r45, %r44, %r43;
|
|
||||||
shr.u32 %r46, %r45, 31;
|
|
||||||
shr.u32 %r47, %r45, 7;
|
|
||||||
add.s32 %r48, %r47, %r46;
|
|
||||||
st.global.u8 [%rd13], %r48;
|
|
||||||
|
|
||||||
$L__BB1_4:
|
|
||||||
ret;
|
|
||||||
|
|
||||||
}
|
|
||||||
// .globl blend_nv12
|
|
||||||
.visible .entry blend_nv12(
|
|
||||||
.param .u64 blend_nv12_param_0,
|
|
||||||
.param .u32 blend_nv12_param_1,
|
|
||||||
.param .u64 blend_nv12_param_2,
|
|
||||||
.param .u32 blend_nv12_param_3,
|
|
||||||
.param .u32 blend_nv12_param_4,
|
|
||||||
.param .u32 blend_nv12_param_5,
|
|
||||||
.param .u64 blend_nv12_param_6,
|
|
||||||
.param .u32 blend_nv12_param_7,
|
|
||||||
.param .u32 blend_nv12_param_8,
|
|
||||||
.param .u32 blend_nv12_param_9,
|
|
||||||
.param .u32 blend_nv12_param_10
|
|
||||||
)
|
|
||||||
{
|
|
||||||
.reg .pred %p<43>;
|
|
||||||
.reg .b16 %rs<17>;
|
|
||||||
.reg .f32 %f<108>;
|
|
||||||
.reg .b32 %r<123>;
|
|
||||||
.reg .b64 %rd<35>;
|
|
||||||
|
|
||||||
|
|
||||||
ld.param.u64 %rd11, [blend_nv12_param_0];
|
|
||||||
ld.param.u32 %r21, [blend_nv12_param_1];
|
|
||||||
ld.param.u64 %rd10, [blend_nv12_param_2];
|
|
||||||
ld.param.u32 %r22, [blend_nv12_param_3];
|
|
||||||
ld.param.u32 %r23, [blend_nv12_param_4];
|
|
||||||
ld.param.u32 %r24, [blend_nv12_param_5];
|
|
||||||
ld.param.u64 %rd12, [blend_nv12_param_6];
|
|
||||||
ld.param.u32 %r25, [blend_nv12_param_7];
|
|
||||||
ld.param.u32 %r26, [blend_nv12_param_8];
|
|
||||||
ld.param.u32 %r27, [blend_nv12_param_9];
|
|
||||||
ld.param.u32 %r28, [blend_nv12_param_10];
|
|
||||||
cvta.to.global.u64 %rd1, %rd11;
|
|
||||||
cvta.to.global.u64 %rd2, %rd12;
|
|
||||||
mov.u32 %r29, %ntid.x;
|
|
||||||
mov.u32 %r30, %ctaid.x;
|
|
||||||
mov.u32 %r31, %tid.x;
|
|
||||||
mad.lo.s32 %r32, %r30, %r29, %r31;
|
|
||||||
mov.u32 %r33, %ntid.y;
|
|
||||||
mov.u32 %r34, %ctaid.y;
|
|
||||||
mov.u32 %r35, %tid.y;
|
|
||||||
mad.lo.s32 %r36, %r34, %r33, %r35;
|
|
||||||
shl.b32 %r1, %r32, 1;
|
|
||||||
shl.b32 %r2, %r36, 1;
|
|
||||||
setp.ge.s32 %p1, %r1, %r25;
|
|
||||||
setp.ge.s32 %p2, %r2, %r26;
|
|
||||||
or.pred %p3, %p1, %p2;
|
|
||||||
mov.f32 %f102, 0f00000000;
|
|
||||||
mov.f32 %f103, 0f00000000;
|
|
||||||
mov.f32 %f104, 0f00000000;
|
|
||||||
@%p3 bra $L__BB2_19;
|
|
||||||
|
|
||||||
cvt.s64.s32 %rd3, %r21;
|
|
||||||
add.s32 %r3, %r2, %r28;
|
|
||||||
setp.ge.s32 %p4, %r3, %r24;
|
|
||||||
mul.lo.s32 %r4, %r2, %r25;
|
|
||||||
mul.wide.s32 %rd4, %r3, %r21;
|
|
||||||
add.s32 %r5, %r1, %r27;
|
|
||||||
or.b32 %r38, %r5, %r3;
|
|
||||||
setp.lt.s32 %p5, %r38, 0;
|
|
||||||
mov.u32 %r121, 0;
|
|
||||||
setp.ge.s32 %p6, %r5, %r23;
|
|
||||||
or.pred %p7, %p6, %p5;
|
|
||||||
or.pred %p8, %p4, %p7;
|
|
||||||
@%p8 bra $L__BB2_4;
|
|
||||||
|
|
||||||
add.s32 %r40, %r1, %r4;
|
|
||||||
mul.wide.s32 %rd13, %r40, 4;
|
|
||||||
add.s64 %rd5, %rd2, %rd13;
|
|
||||||
ld.global.u8 %rs1, [%rd5+3];
|
|
||||||
setp.eq.s16 %p9, %rs1, 0;
|
|
||||||
@%p9 bra $L__BB2_4;
|
|
||||||
|
|
||||||
cvt.u32.u16 %r42, %rs1;
|
|
||||||
ld.global.u8 %rs5, [%rd5];
|
|
||||||
cvt.rn.f32.u16 %f31, %rs5;
|
|
||||||
ld.global.u8 %rs6, [%rd5+1];
|
|
||||||
cvt.rn.f32.u16 %f32, %rs6;
|
|
||||||
ld.global.u8 %rs7, [%rd5+2];
|
|
||||||
cvt.rn.f32.u16 %f33, %rs7;
|
|
||||||
fma.rn.f32 %f34, %f31, 0f3E3AFB7F, 0f41800000;
|
|
||||||
fma.rn.f32 %f35, %f32, 0f3F1D3C36, %f34;
|
|
||||||
fma.rn.f32 %f36, %f33, 0f3D7DF3B6, %f35;
|
|
||||||
add.f32 %f37, %f36, 0f3F000000;
|
|
||||||
cvt.rzi.s32.f32 %r43, %f37;
|
|
||||||
cvt.s64.s32 %rd14, %r5;
|
|
||||||
add.s64 %rd15, %rd4, %rd14;
|
|
||||||
add.s64 %rd16, %rd1, %rd15;
|
|
||||||
ld.global.u8 %r44, [%rd16];
|
|
||||||
mul.lo.s32 %r45, %r43, %r42;
|
|
||||||
xor.b32 %r46, %r42, 255;
|
|
||||||
mad.lo.s32 %r47, %r46, %r44, %r45;
|
|
||||||
mul.hi.s32 %r48, %r47, -2139062143;
|
|
||||||
add.s32 %r49, %r48, %r47;
|
|
||||||
shr.u32 %r50, %r49, 31;
|
|
||||||
shr.u32 %r51, %r49, 7;
|
|
||||||
add.s32 %r52, %r51, %r50;
|
|
||||||
st.global.u8 [%rd16], %r52;
|
|
||||||
fma.rn.f32 %f38, %f31, 0fBDCE075F, 0f43000000;
|
|
||||||
fma.rn.f32 %f39, %f32, 0fBEAD5CFB, %f38;
|
|
||||||
fma.rn.f32 %f40, %f33, 0f3EE0DED3, %f39;
|
|
||||||
cvt.rn.f32.u16 %f104, %rs1;
|
|
||||||
fma.rn.f32 %f102, %f40, %f104, 0f00000000;
|
|
||||||
fma.rn.f32 %f41, %f31, 0f3EE0DED3, 0f43000000;
|
|
||||||
fma.rn.f32 %f42, %f32, 0fBECC3C9F, %f41;
|
|
||||||
fma.rn.f32 %f43, %f33, 0fBD25119D, %f42;
|
|
||||||
fma.rn.f32 %f103, %f43, %f104, 0f00000000;
|
|
||||||
mov.u32 %r121, 1;
|
|
||||||
|
|
||||||
$L__BB2_4:
|
|
||||||
add.s32 %r7, %r1, 1;
|
|
||||||
setp.ge.s32 %p10, %r7, %r25;
|
|
||||||
@%p10 bra $L__BB2_8;
|
|
||||||
|
|
||||||
add.s32 %r8, %r7, %r27;
|
|
||||||
or.b32 %r53, %r8, %r3;
|
|
||||||
setp.lt.s32 %p12, %r53, 0;
|
|
||||||
setp.ge.s32 %p13, %r8, %r23;
|
|
||||||
or.pred %p14, %p13, %p12;
|
|
||||||
or.pred %p15, %p4, %p14;
|
|
||||||
@%p15 bra $L__BB2_8;
|
|
||||||
|
|
||||||
add.s32 %r54, %r7, %r4;
|
|
||||||
mul.wide.s32 %rd17, %r54, 4;
|
|
||||||
add.s64 %rd6, %rd2, %rd17;
|
|
||||||
ld.global.u8 %rs2, [%rd6+3];
|
|
||||||
setp.eq.s16 %p16, %rs2, 0;
|
|
||||||
@%p16 bra $L__BB2_8;
|
|
||||||
|
|
||||||
cvt.u32.u16 %r55, %rs2;
|
|
||||||
ld.global.u8 %rs8, [%rd6];
|
|
||||||
cvt.rn.f32.u16 %f44, %rs8;
|
|
||||||
ld.global.u8 %rs9, [%rd6+1];
|
|
||||||
cvt.rn.f32.u16 %f45, %rs9;
|
|
||||||
ld.global.u8 %rs10, [%rd6+2];
|
|
||||||
cvt.rn.f32.u16 %f46, %rs10;
|
|
||||||
fma.rn.f32 %f47, %f44, 0f3E3AFB7F, 0f41800000;
|
|
||||||
fma.rn.f32 %f48, %f45, 0f3F1D3C36, %f47;
|
|
||||||
fma.rn.f32 %f49, %f46, 0f3D7DF3B6, %f48;
|
|
||||||
add.f32 %f50, %f49, 0f3F000000;
|
|
||||||
cvt.rzi.s32.f32 %r56, %f50;
|
|
||||||
cvt.s64.s32 %rd18, %r8;
|
|
||||||
add.s64 %rd19, %rd4, %rd18;
|
|
||||||
add.s64 %rd20, %rd1, %rd19;
|
|
||||||
ld.global.u8 %r57, [%rd20];
|
|
||||||
mul.lo.s32 %r58, %r56, %r55;
|
|
||||||
xor.b32 %r59, %r55, 255;
|
|
||||||
mad.lo.s32 %r60, %r59, %r57, %r58;
|
|
||||||
mul.hi.s32 %r61, %r60, -2139062143;
|
|
||||||
add.s32 %r62, %r61, %r60;
|
|
||||||
shr.u32 %r63, %r62, 31;
|
|
||||||
shr.u32 %r64, %r62, 7;
|
|
||||||
add.s32 %r65, %r64, %r63;
|
|
||||||
st.global.u8 [%rd20], %r65;
|
|
||||||
fma.rn.f32 %f51, %f44, 0fBDCE075F, 0f43000000;
|
|
||||||
fma.rn.f32 %f52, %f45, 0fBEAD5CFB, %f51;
|
|
||||||
fma.rn.f32 %f53, %f46, 0f3EE0DED3, %f52;
|
|
||||||
cvt.rn.f32.u16 %f54, %rs2;
|
|
||||||
fma.rn.f32 %f102, %f53, %f54, %f102;
|
|
||||||
fma.rn.f32 %f55, %f44, 0f3EE0DED3, 0f43000000;
|
|
||||||
fma.rn.f32 %f56, %f45, 0fBECC3C9F, %f55;
|
|
||||||
fma.rn.f32 %f57, %f46, 0fBD25119D, %f56;
|
|
||||||
fma.rn.f32 %f103, %f57, %f54, %f103;
|
|
||||||
add.f32 %f104, %f104, %f54;
|
|
||||||
add.s32 %r121, %r121, 1;
|
|
||||||
|
|
||||||
$L__BB2_8:
|
|
||||||
add.s32 %r11, %r2, 1;
|
|
||||||
setp.ge.s32 %p17, %r11, %r26;
|
|
||||||
add.s32 %r12, %r11, %r28;
|
|
||||||
add.s32 %r13, %r4, %r25;
|
|
||||||
cvt.s64.s32 %rd21, %r12;
|
|
||||||
mul.lo.s64 %rd7, %rd21, %rd3;
|
|
||||||
@%p17 bra $L__BB2_12;
|
|
||||||
|
|
||||||
setp.ge.s32 %p18, %r12, %r24;
|
|
||||||
or.b32 %r66, %r5, %r12;
|
|
||||||
setp.lt.s32 %p19, %r66, 0;
|
|
||||||
or.pred %p21, %p6, %p19;
|
|
||||||
or.pred %p22, %p18, %p21;
|
|
||||||
@%p22 bra $L__BB2_12;
|
|
||||||
|
|
||||||
add.s32 %r67, %r1, %r13;
|
|
||||||
mul.wide.s32 %rd22, %r67, 4;
|
|
||||||
add.s64 %rd8, %rd2, %rd22;
|
|
||||||
ld.global.u8 %rs3, [%rd8+3];
|
|
||||||
setp.eq.s16 %p23, %rs3, 0;
|
|
||||||
@%p23 bra $L__BB2_12;
|
|
||||||
|
|
||||||
cvt.u32.u16 %r68, %rs3;
|
|
||||||
ld.global.u8 %rs11, [%rd8];
|
|
||||||
cvt.rn.f32.u16 %f58, %rs11;
|
|
||||||
ld.global.u8 %rs12, [%rd8+1];
|
|
||||||
cvt.rn.f32.u16 %f59, %rs12;
|
|
||||||
ld.global.u8 %rs13, [%rd8+2];
|
|
||||||
cvt.rn.f32.u16 %f60, %rs13;
|
|
||||||
fma.rn.f32 %f61, %f58, 0f3E3AFB7F, 0f41800000;
|
|
||||||
fma.rn.f32 %f62, %f59, 0f3F1D3C36, %f61;
|
|
||||||
fma.rn.f32 %f63, %f60, 0f3D7DF3B6, %f62;
|
|
||||||
add.f32 %f64, %f63, 0f3F000000;
|
|
||||||
cvt.rzi.s32.f32 %r69, %f64;
|
|
||||||
cvt.s64.s32 %rd23, %r5;
|
|
||||||
add.s64 %rd24, %rd7, %rd23;
|
|
||||||
add.s64 %rd25, %rd1, %rd24;
|
|
||||||
ld.global.u8 %r70, [%rd25];
|
|
||||||
mul.lo.s32 %r71, %r69, %r68;
|
|
||||||
xor.b32 %r72, %r68, 255;
|
|
||||||
mad.lo.s32 %r73, %r72, %r70, %r71;
|
|
||||||
mul.hi.s32 %r74, %r73, -2139062143;
|
|
||||||
add.s32 %r75, %r74, %r73;
|
|
||||||
shr.u32 %r76, %r75, 31;
|
|
||||||
shr.u32 %r77, %r75, 7;
|
|
||||||
add.s32 %r78, %r77, %r76;
|
|
||||||
st.global.u8 [%rd25], %r78;
|
|
||||||
fma.rn.f32 %f65, %f58, 0fBDCE075F, 0f43000000;
|
|
||||||
fma.rn.f32 %f66, %f59, 0fBEAD5CFB, %f65;
|
|
||||||
fma.rn.f32 %f67, %f60, 0f3EE0DED3, %f66;
|
|
||||||
cvt.rn.f32.u16 %f68, %rs3;
|
|
||||||
fma.rn.f32 %f102, %f67, %f68, %f102;
|
|
||||||
fma.rn.f32 %f69, %f58, 0f3EE0DED3, 0f43000000;
|
|
||||||
fma.rn.f32 %f70, %f59, 0fBECC3C9F, %f69;
|
|
||||||
fma.rn.f32 %f71, %f60, 0fBD25119D, %f70;
|
|
||||||
fma.rn.f32 %f103, %f71, %f68, %f103;
|
|
||||||
add.f32 %f104, %f104, %f68;
|
|
||||||
add.s32 %r121, %r121, 1;
|
|
||||||
|
|
||||||
$L__BB2_12:
|
|
||||||
or.pred %p26, %p17, %p10;
|
|
||||||
@%p26 bra $L__BB2_16;
|
|
||||||
|
|
||||||
setp.ge.s32 %p27, %r12, %r24;
|
|
||||||
add.s32 %r16, %r7, %r27;
|
|
||||||
or.b32 %r79, %r16, %r12;
|
|
||||||
setp.lt.s32 %p28, %r79, 0;
|
|
||||||
setp.ge.s32 %p29, %r16, %r23;
|
|
||||||
or.pred %p30, %p29, %p28;
|
|
||||||
or.pred %p31, %p27, %p30;
|
|
||||||
@%p31 bra $L__BB2_16;
|
|
||||||
|
|
||||||
add.s32 %r80, %r7, %r13;
|
|
||||||
mul.wide.s32 %rd26, %r80, 4;
|
|
||||||
add.s64 %rd9, %rd2, %rd26;
|
|
||||||
ld.global.u8 %rs4, [%rd9+3];
|
|
||||||
setp.eq.s16 %p32, %rs4, 0;
|
|
||||||
@%p32 bra $L__BB2_16;
|
|
||||||
|
|
||||||
cvt.u32.u16 %r81, %rs4;
|
|
||||||
ld.global.u8 %rs14, [%rd9];
|
|
||||||
cvt.rn.f32.u16 %f72, %rs14;
|
|
||||||
ld.global.u8 %rs15, [%rd9+1];
|
|
||||||
cvt.rn.f32.u16 %f73, %rs15;
|
|
||||||
ld.global.u8 %rs16, [%rd9+2];
|
|
||||||
cvt.rn.f32.u16 %f74, %rs16;
|
|
||||||
fma.rn.f32 %f75, %f72, 0f3E3AFB7F, 0f41800000;
|
|
||||||
fma.rn.f32 %f76, %f73, 0f3F1D3C36, %f75;
|
|
||||||
fma.rn.f32 %f77, %f74, 0f3D7DF3B6, %f76;
|
|
||||||
add.f32 %f78, %f77, 0f3F000000;
|
|
||||||
cvt.rzi.s32.f32 %r82, %f78;
|
|
||||||
cvt.s64.s32 %rd27, %r16;
|
|
||||||
add.s64 %rd28, %rd7, %rd27;
|
|
||||||
add.s64 %rd29, %rd1, %rd28;
|
|
||||||
ld.global.u8 %r83, [%rd29];
|
|
||||||
mul.lo.s32 %r84, %r82, %r81;
|
|
||||||
xor.b32 %r85, %r81, 255;
|
|
||||||
mad.lo.s32 %r86, %r85, %r83, %r84;
|
|
||||||
mul.hi.s32 %r87, %r86, -2139062143;
|
|
||||||
add.s32 %r88, %r87, %r86;
|
|
||||||
shr.u32 %r89, %r88, 31;
|
|
||||||
shr.u32 %r90, %r88, 7;
|
|
||||||
add.s32 %r91, %r90, %r89;
|
|
||||||
st.global.u8 [%rd29], %r91;
|
|
||||||
fma.rn.f32 %f79, %f72, 0fBDCE075F, 0f43000000;
|
|
||||||
fma.rn.f32 %f80, %f73, 0fBEAD5CFB, %f79;
|
|
||||||
fma.rn.f32 %f81, %f74, 0f3EE0DED3, %f80;
|
|
||||||
cvt.rn.f32.u16 %f82, %rs4;
|
|
||||||
fma.rn.f32 %f102, %f81, %f82, %f102;
|
|
||||||
fma.rn.f32 %f83, %f72, 0f3EE0DED3, 0f43000000;
|
|
||||||
fma.rn.f32 %f84, %f73, 0fBECC3C9F, %f83;
|
|
||||||
fma.rn.f32 %f85, %f74, 0fBD25119D, %f84;
|
|
||||||
fma.rn.f32 %f103, %f85, %f82, %f103;
|
|
||||||
add.f32 %f104, %f104, %f82;
|
|
||||||
add.s32 %r121, %r121, 1;
|
|
||||||
|
|
||||||
$L__BB2_16:
|
|
||||||
setp.eq.s32 %p33, %r121, 0;
|
|
||||||
setp.le.f32 %p34, %f104, 0f00000000;
|
|
||||||
or.pred %p35, %p34, %p33;
|
|
||||||
@%p35 bra $L__BB2_19;
|
|
||||||
|
|
||||||
shr.u32 %r92, %r5, 31;
|
|
||||||
add.s32 %r93, %r5, %r92;
|
|
||||||
shr.s32 %r19, %r93, 1;
|
|
||||||
setp.lt.s32 %p36, %r3, -1;
|
|
||||||
setp.lt.s32 %p37, %r5, -1;
|
|
||||||
or.pred %p38, %p37, %p36;
|
|
||||||
and.b32 %r94, %r93, -2;
|
|
||||||
setp.ge.s32 %p39, %r94, %r23;
|
|
||||||
or.pred %p40, %p38, %p39;
|
|
||||||
shr.u32 %r95, %r3, 31;
|
|
||||||
add.s32 %r96, %r3, %r95;
|
|
||||||
shr.s32 %r20, %r96, 1;
|
|
||||||
and.b32 %r97, %r96, -2;
|
|
||||||
setp.ge.s32 %p41, %r97, %r24;
|
|
||||||
or.pred %p42, %p40, %p41;
|
|
||||||
@%p42 bra $L__BB2_19;
|
|
||||||
|
|
||||||
div.rn.f32 %f86, %f102, %f104;
|
|
||||||
add.f32 %f87, %f86, 0f3F000000;
|
|
||||||
cvt.rzi.s32.f32 %r98, %f87;
|
|
||||||
div.rn.f32 %f88, %f103, %f104;
|
|
||||||
add.f32 %f89, %f88, 0f3F000000;
|
|
||||||
cvt.rzi.s32.f32 %r99, %f89;
|
|
||||||
cvt.rn.f32.s32 %f90, %r121;
|
|
||||||
div.rn.f32 %f91, %f104, %f90;
|
|
||||||
add.f32 %f92, %f91, 0f3F000000;
|
|
||||||
cvt.rzi.s32.f32 %r100, %f92;
|
|
||||||
mul.wide.s32 %rd30, %r20, %r22;
|
|
||||||
mul.wide.s32 %rd31, %r19, 2;
|
|
||||||
add.s64 %rd32, %rd30, %rd31;
|
|
||||||
cvta.to.global.u64 %rd33, %rd10;
|
|
||||||
add.s64 %rd34, %rd33, %rd32;
|
|
||||||
ld.global.u8 %r101, [%rd34];
|
|
||||||
mul.lo.s32 %r102, %r100, %r98;
|
|
||||||
mov.u32 %r103, 255;
|
|
||||||
sub.s32 %r104, %r103, %r100;
|
|
||||||
mad.lo.s32 %r105, %r104, %r101, %r102;
|
|
||||||
mul.hi.s32 %r106, %r105, -2139062143;
|
|
||||||
add.s32 %r107, %r106, %r105;
|
|
||||||
shr.u32 %r108, %r107, 31;
|
|
||||||
shr.u32 %r109, %r107, 7;
|
|
||||||
add.s32 %r110, %r109, %r108;
|
|
||||||
st.global.u8 [%rd34], %r110;
|
|
||||||
ld.global.u8 %r111, [%rd34+1];
|
|
||||||
mul.lo.s32 %r112, %r100, %r99;
|
|
||||||
mad.lo.s32 %r113, %r104, %r111, %r112;
|
|
||||||
mul.hi.s32 %r114, %r113, -2139062143;
|
|
||||||
add.s32 %r115, %r114, %r113;
|
|
||||||
shr.u32 %r116, %r115, 31;
|
|
||||||
shr.u32 %r117, %r115, 7;
|
|
||||||
add.s32 %r118, %r117, %r116;
|
|
||||||
st.global.u8 [%rd34+1], %r118;
|
|
||||||
|
|
||||||
$L__BB2_19:
|
|
||||||
ret;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -69,6 +69,7 @@ use super::{AuChunk, ChromaFormat, Codec, EncodedFrame, Encoder, EncoderCaps};
|
|||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use pf_frame::{CapturedFrame, FramePayload};
|
use pf_frame::{CapturedFrame, FramePayload};
|
||||||
use pf_zerocopy::cuda::{self, InputSurface};
|
use pf_zerocopy::cuda::{self, InputSurface};
|
||||||
|
use pf_zerocopy::vkslot::{SlotFormat, VkSlotBlend, VkSlotRef};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
use std::ptr;
|
use std::ptr;
|
||||||
@@ -76,16 +77,6 @@ use std::sync::mpsc;
|
|||||||
|
|
||||||
use nvidia_video_codec_sdk::sys::nvEncodeAPI as nv;
|
use nvidia_video_codec_sdk::sys::nvEncodeAPI as nv;
|
||||||
|
|
||||||
/// Prebuilt PTX for the cursor-overlay blend kernels (cursor-as-metadata). Source is
|
|
||||||
/// `cursor_blend.cu` beside this file; regenerate with
|
|
||||||
/// `nvcc -ptx -arch=compute_75 cursor_blend.cu -o cursor_blend.ptx` after editing. JIT'd by the
|
|
||||||
/// driver, so it runs on any Turing-or-newer GPU. ⚠️ The `.version` stamp is load-bearing (see
|
|
||||||
/// the comment in the .ptx): a driver refuses PTX with an ISA newer than its JIT (error 222),
|
|
||||||
/// and a stamp older than the body's real syntax is INVALID_PTX (218) — so a new toolkit's
|
|
||||||
/// output silently kills cursor compositing on older-driver boxes. Prefer regenerating with the
|
|
||||||
/// OLDEST toolchain that compiles the .cu, and re-test on the oldest driver box.
|
|
||||||
const CURSOR_PTX: &[u8] = include_bytes!("cursor_blend.ptx");
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------
|
||||||
// Runtime-loaded NVENC entry table (Linux). Same shape as the Windows backend's `EncodeApi`, minus
|
// Runtime-loaded NVENC entry table (Linux). Same shape as the Windows backend's `EncodeApi`, minus
|
||||||
// the async-event entry points (Windows-only). Resolved once from `libnvidia-encode.so.1` — the two
|
// the async-event entry points (Windows-only). Resolved once from `libnvidia-encode.so.1` — the two
|
||||||
@@ -432,13 +423,54 @@ fn buffer_format(buf: &cuda::DeviceBuffer) -> nv::NV_ENC_BUFFER_FORMAT {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One encoder-owned CUDA input surface + its NVENC registration. The surface is copied into each
|
/// One encoder-owned input surface + its NVENC registration. The surface is copied into each
|
||||||
/// use (device→device) and the registration is created once at session init, unregistered at teardown.
|
/// use (device→device) and the registration is created once at session init, unregistered at teardown.
|
||||||
struct RingSlot {
|
struct RingSlot {
|
||||||
surface: InputSurface,
|
surface: SlotSurface,
|
||||||
reg: nv::NV_ENC_REGISTERED_PTR,
|
reg: nv::NV_ENC_REGISTERED_PTR,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The ring slot's backing allocation: Vulkan external memory CUDA-imported (the normal case —
|
||||||
|
/// blendable by the SPIR-V cursor pass, see `vkslot.rs`) or a plain pitched CUDA allocation (the
|
||||||
|
/// fallback when Vulkan bring-up fails: sessions still encode, composite mode just has no
|
||||||
|
/// cursor). Both present the same `(ptr, pitch, height)` NVENC-registration vocabulary.
|
||||||
|
enum SlotSurface {
|
||||||
|
Cuda(InputSurface),
|
||||||
|
/// Backing objects live in the encoder's [`VkSlotBlend`] (freed by its `free_slots`); the
|
||||||
|
/// ref itself is Copy and carries the registered geometry.
|
||||||
|
Vk(VkSlotRef),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The [`SlotFormat`] for an NVENC buffer format (the ring-build + blend vocabulary).
|
||||||
|
fn slot_fmt_of(fmt: nv::NV_ENC_BUFFER_FORMAT) -> SlotFormat {
|
||||||
|
match fmt {
|
||||||
|
nv::NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV444 => SlotFormat::Yuv444,
|
||||||
|
nv::NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_NV12 => SlotFormat::Nv12,
|
||||||
|
_ => SlotFormat::Argb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlotSurface {
|
||||||
|
fn ptr(&self) -> pf_zerocopy::cuda::CUdeviceptr {
|
||||||
|
match self {
|
||||||
|
SlotSurface::Cuda(s) => s.ptr,
|
||||||
|
SlotSurface::Vk(r) => r.ptr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn pitch(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
SlotSurface::Cuda(s) => s.pitch,
|
||||||
|
SlotSurface::Vk(r) => r.pitch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn height(&self) -> u32 {
|
||||||
|
match self {
|
||||||
|
SlotSurface::Cuda(s) => s.height,
|
||||||
|
SlotSurface::Vk(r) => r.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `doNotWait` sampling cadence inside [`Encoder::poll_chunk`] — the probe measured ~200 µs
|
/// `doNotWait` sampling cadence inside [`Encoder::poll_chunk`] — the probe measured ~200 µs
|
||||||
/// between slice completions on the 5070 Ti, so 50 µs keeps the added per-chunk delivery delay
|
/// between slice completions on the 5070 Ti, so 50 µs keeps the added per-chunk delivery delay
|
||||||
/// well under one slice time without hammering the driver.
|
/// well under one slice time without hammering the driver.
|
||||||
@@ -534,16 +566,18 @@ pub struct NvencCudaEncoder {
|
|||||||
split_mode: u32,
|
split_mode: u32,
|
||||||
/// The last reference-frame range we invalidated — dedupes repeated RFI requests for one loss.
|
/// The last reference-frame range we invalidated — dedupes repeated RFI requests for one loss.
|
||||||
last_rfi_range: Option<(i64, i64)>,
|
last_rfi_range: Option<(i64, i64)>,
|
||||||
/// Cursor-as-metadata GPU blend (loaded lazily on the first frame that carries a cursor, once the
|
/// Cursor-as-metadata GPU blend: the Vulkan device + SPIR-V compute pass the ring's
|
||||||
/// CUDA context is current). `None` until then or if the module load fails; `cursor_tried` stops
|
/// external-memory slots are allocated through (`vkslot.rs`) — the driver-portable
|
||||||
/// re-attempting a failed load every frame. `cursor_serial` tracks the uploaded bitmap.
|
/// replacement for the retired PTX kernels. Brought up once at session init (`cursor_tried`
|
||||||
cursor: Option<cuda::CursorBlend>,
|
/// stops re-attempts); `None` = bring-up failed, the ring fell back to plain CUDA
|
||||||
|
/// allocations and composite mode degrades to no cursor. `cursor_serial` tracks the
|
||||||
|
/// uploaded bitmap.
|
||||||
|
vk_blend: Option<VkSlotBlend>,
|
||||||
cursor_tried: bool,
|
cursor_tried: bool,
|
||||||
cursor_serial: u64,
|
cursor_serial: u64,
|
||||||
/// Suppress-until-success latches for the per-frame cursor upload/blend warns: a persistent
|
/// Suppress-until-success latch for the per-frame blend warn: a persistent failure sits in
|
||||||
/// failure sits in the submit() hot path, so warn once per failure streak (reset on success)
|
/// the submit() hot path, so warn once per failure streak (reset on success) rather than on
|
||||||
/// rather than on every cursor-bearing frame, which would evict the log ring.
|
/// every cursor-bearing frame, which would evict the log ring.
|
||||||
cursor_upload_warned: bool,
|
|
||||||
cursor_blend_warned: bool,
|
cursor_blend_warned: bool,
|
||||||
/// One-shot latch for [`diagnose_failed_open`](Self::diagnose_failed_open) so a rebuild-retry
|
/// One-shot latch for [`diagnose_failed_open`](Self::diagnose_failed_open) so a rebuild-retry
|
||||||
/// burst (the session loop's bounded encoder resets) logs the diagnosis once, not per attempt.
|
/// burst (the session loop's bounded encoder resets) logs the diagnosis once, not per attempt.
|
||||||
@@ -649,10 +683,9 @@ impl NvencCudaEncoder {
|
|||||||
frame_idx: 0,
|
frame_idx: 0,
|
||||||
force_kf: false,
|
force_kf: false,
|
||||||
pending_anchor: false,
|
pending_anchor: false,
|
||||||
cursor: None,
|
vk_blend: None,
|
||||||
cursor_tried: false,
|
cursor_tried: false,
|
||||||
cursor_serial: u64::MAX,
|
cursor_serial: u64::MAX,
|
||||||
cursor_upload_warned: false,
|
|
||||||
cursor_blend_warned: false,
|
cursor_blend_warned: false,
|
||||||
diagnosed: false,
|
diagnosed: false,
|
||||||
inited: false,
|
inited: false,
|
||||||
@@ -743,7 +776,12 @@ impl NvencCudaEncoder {
|
|||||||
// (the forfeit contract), and the next session re-latches the arming at init.
|
// (the forfeit contract), and the next session re-latches the arming at init.
|
||||||
self.subframe_chunks = false;
|
self.subframe_chunks = false;
|
||||||
self.chunk = None;
|
self.chunk = None;
|
||||||
self.ring.clear(); // drops the InputSurfaces, freeing their CUDA allocations
|
self.ring.clear(); // drops the CUDA InputSurfaces; Vk slots are freed just below
|
||||||
|
if let Some(vk) = &mut self.vk_blend {
|
||||||
|
// The Vulkan-backed slots' memory (and its CUDA mapping) — the device itself stays
|
||||||
|
// up for the next session's ring (`cursor_tried` keeps bring-up one-shot).
|
||||||
|
vk.free_slots();
|
||||||
|
}
|
||||||
self.bitstreams.clear();
|
self.bitstreams.clear();
|
||||||
self.pending.clear();
|
self.pending.clear();
|
||||||
self.encoder = ptr::null_mut();
|
self.encoder = ptr::null_mut();
|
||||||
@@ -1132,8 +1170,54 @@ impl NvencCudaEncoder {
|
|||||||
|
|
||||||
// Encoder-owned input-surface ring: allocate + register POOL surfaces in the negotiated
|
// Encoder-owned input-surface ring: allocate + register POOL surfaces in the negotiated
|
||||||
// format. Registered once here, mapped per submit, unregistered at teardown.
|
// format. Registered once here, mapped per submit, unregistered at teardown.
|
||||||
|
// Preferred backing = Vulkan external memory CUDA-imported (`vkslot.rs`), so the
|
||||||
|
// SPIR-V cursor blend can composite into the very bytes NVENC encodes; any bring-up
|
||||||
|
// or per-slot failure falls back to plain pitched CUDA allocations (sessions always
|
||||||
|
// encode — composite mode just loses the cursor, warned below).
|
||||||
|
if !self.cursor_tried {
|
||||||
|
self.cursor_tried = true;
|
||||||
|
match VkSlotBlend::new() {
|
||||||
|
Ok(v) => self.vk_blend = Some(v),
|
||||||
|
Err(e) => tracing::warn!(
|
||||||
|
error = %format!("{e:#}"),
|
||||||
|
"NVENC (Linux): Vulkan slot-blend bring-up failed — plain CUDA input \
|
||||||
|
surfaces, cursor compositing unavailable"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let slot_fmt = slot_fmt_of(self.buffer_fmt);
|
||||||
|
// Two attempts: the full ring on Vulkan slots, else (any failure) the full ring on
|
||||||
|
// plain CUDA — never a mixed ring (it would blend on some slots only: a flickering
|
||||||
|
// cursor) and never a short one.
|
||||||
|
'ring: for use_vk in [self.vk_blend.is_some(), false] {
|
||||||
|
if !use_vk && self.vk_blend.is_some() {
|
||||||
|
// Second attempt: retire the Vulkan side wholesale first.
|
||||||
|
for s in self.ring.drain(..) {
|
||||||
|
let _ = (api().unregister_resource)(self.encoder, s.reg);
|
||||||
|
}
|
||||||
|
if let Some(vk) = &mut self.vk_blend {
|
||||||
|
vk.free_slots();
|
||||||
|
}
|
||||||
|
self.vk_blend = None;
|
||||||
|
}
|
||||||
for _ in 0..POOL {
|
for _ in 0..POOL {
|
||||||
let surface = match self.buffer_fmt {
|
let surface = if use_vk {
|
||||||
|
let vk = self.vk_blend.as_mut().expect("use_vk implies Some");
|
||||||
|
match vk.alloc_slot(slot_fmt, self.width, self.height) {
|
||||||
|
Ok(r) => SlotSurface::Vk(r),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
error = %format!("{e:#}"),
|
||||||
|
"NVENC (Linux): Vulkan slot alloc failed — rebuilding the \
|
||||||
|
ring on plain CUDA surfaces (cursor compositing \
|
||||||
|
unavailable)"
|
||||||
|
);
|
||||||
|
continue 'ring;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SlotSurface::Cuda(
|
||||||
|
match self.buffer_fmt {
|
||||||
nv::NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV444 => {
|
nv::NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV444 => {
|
||||||
InputSurface::alloc_yuv444(self.width, self.height)
|
InputSurface::alloc_yuv444(self.width, self.height)
|
||||||
}
|
}
|
||||||
@@ -1142,27 +1226,47 @@ impl NvencCudaEncoder {
|
|||||||
}
|
}
|
||||||
_ => InputSurface::alloc_rgb(self.width, self.height),
|
_ => InputSurface::alloc_rgb(self.width, self.height),
|
||||||
}
|
}
|
||||||
.context("alloc NVENC input surface")?;
|
.context("alloc NVENC input surface")?,
|
||||||
|
)
|
||||||
|
};
|
||||||
let mut rr = nv::NV_ENC_REGISTER_RESOURCE {
|
let mut rr = nv::NV_ENC_REGISTER_RESOURCE {
|
||||||
version: nv::NV_ENC_REGISTER_RESOURCE_VER,
|
version: nv::NV_ENC_REGISTER_RESOURCE_VER,
|
||||||
resourceType:
|
resourceType:
|
||||||
nv::NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
|
nv::NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
|
||||||
width: self.width,
|
width: self.width,
|
||||||
height: self.height,
|
height: self.height,
|
||||||
pitch: surface.pitch as u32,
|
pitch: surface.pitch() as u32,
|
||||||
resourceToRegister: surface.ptr as *mut c_void,
|
resourceToRegister: surface.ptr() as *mut c_void,
|
||||||
bufferFormat: self.buffer_fmt,
|
bufferFormat: self.buffer_fmt,
|
||||||
bufferUsage: nv::NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
|
bufferUsage: nv::NV_ENC_BUFFER_USAGE::NV_ENC_INPUT_IMAGE,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
(api().register_resource)(self.encoder, &mut rr)
|
match (api().register_resource)(self.encoder, &mut rr).nv_ok() {
|
||||||
.nv_ok()
|
Ok(()) => {}
|
||||||
.map_err(|e| nvenc_status::call_err("register_resource (CUDADEVICEPTR)", e))?;
|
Err(e) if use_vk => {
|
||||||
|
// NVENC refusing the imported pointer is a Vulkan-side condition
|
||||||
|
// too — same wholesale fallback.
|
||||||
|
tracing::warn!(
|
||||||
|
error = ?e,
|
||||||
|
"NVENC (Linux): registering a Vulkan-imported slot failed — \
|
||||||
|
rebuilding the ring on plain CUDA surfaces"
|
||||||
|
);
|
||||||
|
continue 'ring;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(nvenc_status::call_err(
|
||||||
|
"register_resource (CUDADEVICEPTR)",
|
||||||
|
e,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
self.ring.push(RingSlot {
|
self.ring.push(RingSlot {
|
||||||
surface,
|
surface,
|
||||||
reg: rr.registeredResource,
|
reg: rr.registeredResource,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
break 'ring; // full ring built
|
||||||
|
}
|
||||||
|
|
||||||
self.inited = true;
|
self.inited = true;
|
||||||
// Two-thread retrieve (T2.2): spawn the lock thread against the live session. No
|
// Two-thread retrieve (T2.2): spawn the lock thread against the live session. No
|
||||||
@@ -1246,9 +1350,9 @@ impl NvencCudaEncoder {
|
|||||||
/// IO-stream binding (stream-ordered submit — see the gate in [`Encoder::submit`]).
|
/// IO-stream binding (stream-ordered submit — see the gate in [`Encoder::submit`]).
|
||||||
fn copy_into_slot(&self, buf: &cuda::DeviceBuffer, slot: usize, sync: bool) -> Result<()> {
|
fn copy_into_slot(&self, buf: &cuda::DeviceBuffer, slot: usize, sync: bool) -> Result<()> {
|
||||||
let s = &self.ring[slot].surface;
|
let s = &self.ring[slot].surface;
|
||||||
let base = s.ptr;
|
let base = s.ptr();
|
||||||
let pitch = s.pitch;
|
let pitch = s.pitch();
|
||||||
let hh = s.height as u64;
|
let hh = s.height() as u64;
|
||||||
match self.buffer_fmt {
|
match self.buffer_fmt {
|
||||||
nv::NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV444 => {
|
nv::NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV444 => {
|
||||||
if !buf.yuv444 {
|
if !buf.yuv444 {
|
||||||
@@ -1400,79 +1504,57 @@ impl Encoder for NvencCudaEncoder {
|
|||||||
// `async_rt` must be absent too: in two-thread mode the frame may be recycled right after
|
// `async_rt` must be absent too: in two-thread mode the frame may be recycled right after
|
||||||
// submit returns while the stream still holds its copy (belt-and-braces — an escalated
|
// submit returns while the stream still holds its copy (belt-and-braces — an escalated
|
||||||
// session was rebuilt without the binding, so `stream_ordered` is false there anyway).
|
// session was rebuilt without the binding, so `stream_ordered` is false there anyway).
|
||||||
let ordered = self.stream_ordered && self.async_rt.is_none() && self.pending.is_empty();
|
// Cursor-bearing frames additionally force the CPU-synced path: the Vulkan blend sits
|
||||||
|
// between the CUDA copy and the encode, and its cross-API ordering is fence/CPU-
|
||||||
|
// established, not stream-ordered. Frames without a cursor (games hide it; client-draws
|
||||||
|
// sessions strip it) keep the stream-ordered fast path untouched.
|
||||||
|
let ordered = self.stream_ordered
|
||||||
|
&& self.async_rt.is_none()
|
||||||
|
&& self.pending.is_empty()
|
||||||
|
&& captured.cursor.is_none();
|
||||||
let t0 = std::time::Instant::now();
|
let t0 = std::time::Instant::now();
|
||||||
|
|
||||||
// Copy the captured buffer into this slot's input surface before encoding it.
|
// Copy the captured buffer into this slot's input surface before encoding it.
|
||||||
self.copy_into_slot(buf, slot, !ordered)?;
|
self.copy_into_slot(buf, slot, !ordered)?;
|
||||||
let t_copy = t0.elapsed();
|
let t_copy = t0.elapsed();
|
||||||
|
|
||||||
// Cursor-as-metadata: blend the overlay into this slot's OWNED input surface (a tiny kernel
|
// Cursor-as-metadata: blend the overlay into this slot's OWNED input surface via the
|
||||||
// over the cursor's rect — never the compositor's dmabuf). The PTX module loads lazily on the
|
// SPIR-V compute pass (a dispatch over the cursor's rect — never the compositor's
|
||||||
// first cursor frame now that the CUDA context is current; any failure degrades to no cursor,
|
// dmabuf). Cursor-bearing frames forced `ordered = false` above, so the CUDA copy has
|
||||||
// never a dropped frame.
|
// completed before the Vulkan dispatch and the fence-waited dispatch completes before
|
||||||
|
// the encode below — the cross-API ordering is CPU-established. Any failure degrades to
|
||||||
|
// no cursor, never a dropped frame.
|
||||||
if let Some(ov) = &captured.cursor {
|
if let Some(ov) = &captured.cursor {
|
||||||
if !self.cursor_tried {
|
if let (Some(vk), SlotSurface::Vk(vref)) =
|
||||||
self.cursor_tried = true;
|
(self.vk_blend.as_mut(), &self.ring[slot].surface)
|
||||||
match cuda::CursorBlend::new(CURSOR_PTX) {
|
{
|
||||||
Ok(cb) => {
|
|
||||||
// Success is as diagnosis-critical as failure: a silent no-cursor session
|
|
||||||
// must be attributable to "overlay never arrived", not "module never said".
|
|
||||||
tracing::info!("NVENC (Linux): cursor blend module loaded");
|
|
||||||
self.cursor = Some(cb);
|
|
||||||
}
|
|
||||||
Err(e) => tracing::warn!(
|
|
||||||
error = %format!("{e:#}"),
|
|
||||||
"NVENC (Linux): cursor blend module load failed — cursor not composited"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(cb) = &self.cursor {
|
|
||||||
if self.cursor_serial != ov.serial {
|
if self.cursor_serial != ov.serial {
|
||||||
match cb.upload(ov.rgba.as_slice(), ov.w, ov.h) {
|
vk.upload_cursor(ov.rgba.as_slice(), ov.w, ov.h);
|
||||||
Ok(()) => {
|
|
||||||
self.cursor_serial = ov.serial;
|
self.cursor_serial = ov.serial;
|
||||||
self.cursor_upload_warned = false;
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
// surfW = content width; the blend derives plane strides from the slot's luma
|
||||||
if !self.cursor_upload_warned {
|
// height. Cursor pixels past the content land in cropped padding rows — harmless.
|
||||||
self.cursor_upload_warned = true;
|
let r = vk.blend_ref(
|
||||||
tracing::warn!(
|
vref,
|
||||||
error = %format!("{e:#}"),
|
slot_fmt_of(self.buffer_fmt),
|
||||||
serial = ov.serial,
|
self.width,
|
||||||
"NVENC (Linux): cursor upload failed — cursor not composited"
|
ov.w,
|
||||||
|
ov.h,
|
||||||
|
ov.x,
|
||||||
|
ov.y,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let s = &self.ring[slot].surface;
|
|
||||||
// surfW = content width; surfH = the surface's allocated height (matches
|
|
||||||
// `copy_into_slot`'s plane math). Cursor pixels past the content are in cropped
|
|
||||||
// padding rows — harmless.
|
|
||||||
let (w, h) = (self.width, s.height);
|
|
||||||
let r = match self.buffer_fmt {
|
|
||||||
nv::NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_YUV444 => {
|
|
||||||
cb.blend_yuv444(s.ptr, s.pitch, w, h, ov.w, ov.h, ov.x, ov.y, !ordered)
|
|
||||||
}
|
|
||||||
nv::NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_NV12 => {
|
|
||||||
cb.blend_nv12(s.ptr, s.pitch, w, h, ov.w, ov.h, ov.x, ov.y, !ordered)
|
|
||||||
}
|
|
||||||
_ => cb.blend_argb(s.ptr, s.pitch, w, h, ov.w, ov.h, ov.x, ov.y, !ordered),
|
|
||||||
};
|
|
||||||
if let Err(e) = r {
|
if let Err(e) = r {
|
||||||
if !self.cursor_blend_warned {
|
if !self.cursor_blend_warned {
|
||||||
self.cursor_blend_warned = true;
|
self.cursor_blend_warned = true;
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
error = %format!("{e:#}"),
|
error = %format!("{e:#}"),
|
||||||
"NVENC (Linux): cursor blend launch failed — cursor not composited"
|
"NVENC (Linux): cursor blend dispatch failed — cursor not composited"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.cursor_blend_warned = false;
|
self.cursor_blend_warned = false;
|
||||||
// TEMP KWin composite probe (DROP BEFORE MERGE): prove the blend launches and
|
// TEMP KWin composite probe (DROP BEFORE MERGE): prove the blend dispatches
|
||||||
// with what geometry — the on-glass symptom is a loaded module and a silent,
|
// and with what geometry.
|
||||||
// invisible cursor.
|
|
||||||
{
|
{
|
||||||
use std::sync::atomic::{AtomicU64, Ordering as ProbeOrd};
|
use std::sync::atomic::{AtomicU64, Ordering as ProbeOrd};
|
||||||
static PROBE_BLEND: AtomicU64 = AtomicU64::new(0);
|
static PROBE_BLEND: AtomicU64 = AtomicU64::new(0);
|
||||||
@@ -1485,14 +1567,18 @@ impl Encoder for NvencCudaEncoder {
|
|||||||
ov_y = ov.y,
|
ov_y = ov.y,
|
||||||
ov_w = ov.w,
|
ov_w = ov.w,
|
||||||
ov_h = ov.h,
|
ov_h = ov.h,
|
||||||
surf_w = w,
|
|
||||||
surf_h = h,
|
|
||||||
visible = ov.visible,
|
visible = ov.visible,
|
||||||
"cursor blend probe: kernel launched"
|
"cursor blend probe: vulkan dispatch submitted"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if !self.cursor_blend_warned {
|
||||||
|
self.cursor_blend_warned = true;
|
||||||
|
tracing::warn!(
|
||||||
|
"NVENC (Linux): cursor overlay present but no Vulkan blend (bring-up failed \
|
||||||
|
earlier) — cursor not composited"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1540,7 +1626,7 @@ impl Encoder for NvencCudaEncoder {
|
|||||||
version: nv::NV_ENC_PIC_PARAMS_VER,
|
version: nv::NV_ENC_PIC_PARAMS_VER,
|
||||||
inputWidth: self.width,
|
inputWidth: self.width,
|
||||||
inputHeight: self.height,
|
inputHeight: self.height,
|
||||||
inputPitch: self.ring[slot].surface.pitch as u32,
|
inputPitch: self.ring[slot].surface.pitch() as u32,
|
||||||
inputBuffer: mp.mappedResource,
|
inputBuffer: mp.mappedResource,
|
||||||
bufferFmt: mp.mappedBufferFmt,
|
bufferFmt: mp.mappedBufferFmt,
|
||||||
outputBitstream: self.bitstreams[slot],
|
outputBitstream: self.bitstreams[slot],
|
||||||
|
|||||||
@@ -7,8 +7,12 @@
|
|||||||
//! and ffmpeg's `hevc_nvenc` (encode thread) — each thread makes it current before use;
|
//! and ffmpeg's `hevc_nvenc` (encode thread) — each thread makes it current before use;
|
||||||
//! * device memory: pitched allocations, the reusable `BufferPool`/`DeviceBuffer`, IPC
|
//! * device memory: pitched allocations, the reusable `BufferPool`/`DeviceBuffer`, IPC
|
||||||
//! export/import, host readback, and the plane copies;
|
//! export/import, host readback, and the plane copies;
|
||||||
//! * GL / external-memory interop (`RegisteredTexture`, `ExternalDmabuf`); and
|
//! * GL / external-memory interop (`RegisteredTexture`, `ExternalDmabuf`).
|
||||||
//! * the CUDA cursor-blend kernel (`CursorBlend`).
|
//!
|
||||||
|
//! (The CUDA cursor-blend PTX kernel that used to live here is retired: vendored PTX is JIT'd
|
||||||
|
//! against the driver's ISA ceiling and silently dies on older drivers. The NVENC cursor blend
|
||||||
|
//! is now the SPIR-V compute pass in [`super::vkslot`], dispatched over Vulkan-allocated,
|
||||||
|
//! CUDA-imported input slots.)
|
||||||
//!
|
//!
|
||||||
//! (We use GL interop, not EGL interop: `cuGraphicsEGLRegisterImage` is Tegra-only on the desktop
|
//! (We use GL interop, not EGL interop: `cuGraphicsEGLRegisterImage` is Tegra-only on the desktop
|
||||||
//! driver — see [`super::egl`].)
|
//! driver — see [`super::egl`].)
|
||||||
@@ -18,7 +22,6 @@
|
|||||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use std::ffi::CStr;
|
|
||||||
use std::os::raw::{c_uint, c_void};
|
use std::os::raw::{c_uint, c_void};
|
||||||
use std::sync::{Arc, Mutex, OnceLock};
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
@@ -280,258 +283,6 @@ pub fn copy_stream_handle() -> *mut c_void {
|
|||||||
/// Max cursor-overlay bitmap edge (px) uploaded to the device blend buffer — matches the Vulkan path.
|
/// Max cursor-overlay bitmap edge (px) uploaded to the device blend buffer — matches the Vulkan path.
|
||||||
pub const CURSOR_MAX: u32 = 256;
|
pub const CURSOR_MAX: u32 = 256;
|
||||||
|
|
||||||
/// GPU cursor-overlay compositor for the NVENC path (cursor-as-metadata): loads the `cursor_blend`
|
|
||||||
/// PTX module once and blends a straight-alpha RGBA cursor into an encoder-OWNED NVENC input surface
|
|
||||||
/// (ARGB / NV12 / YUV444) with a small kernel launched over the cursor's rectangle — no full-frame
|
|
||||||
/// pass, and the compositor's dmabuf is never touched. The cursor bitmap lives in a device buffer
|
|
||||||
/// re-uploaded only when it changes. Requires `context()` to have succeeded (driver present).
|
|
||||||
pub struct CursorBlend {
|
|
||||||
module: CUmodule,
|
|
||||||
f_argb: CUfunction,
|
|
||||||
f_nv12: CUfunction,
|
|
||||||
f_yuv444: CUfunction,
|
|
||||||
cur_buf: CUdeviceptr, // device RGBA staging (CURSOR_MAX²·4, tight rows)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SAFETY: process-lifetime driver handles used only from the encode thread with the shared context
|
|
||||||
// current — like [`DeviceBuffer`], moving the struct between threads cannot dangle or race.
|
|
||||||
unsafe impl Send for CursorBlend {}
|
|
||||||
|
|
||||||
impl CursorBlend {
|
|
||||||
/// Load the embedded PTX image and resolve the three blend kernels + a device cursor buffer.
|
|
||||||
pub fn new(ptx: &[u8]) -> Result<CursorBlend> {
|
|
||||||
// cuModuleLoadData reads a PTX image as a NUL-terminated string; the embedded .ptx is not,
|
|
||||||
// so append a terminator.
|
|
||||||
let mut image = ptx.to_vec();
|
|
||||||
image.push(0);
|
|
||||||
let mut module: CUmodule = std::ptr::null_mut();
|
|
||||||
// SAFETY: `&mut module` is a live out-param the driver fills; `image` is a NUL-terminated PTX
|
|
||||||
// byte image that outlives the synchronous load. `ck` bails on error before `module` is used.
|
|
||||||
unsafe {
|
|
||||||
ck(
|
|
||||||
cuModuleLoadData(&mut module, image.as_ptr() as *const c_void),
|
|
||||||
"cuModuleLoadData(cursor_blend)",
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
let getf = |name: &CStr| -> Result<CUfunction> {
|
|
||||||
let mut f: CUfunction = std::ptr::null_mut();
|
|
||||||
// SAFETY: `module` loaded above; each name is a valid NUL-terminated symbol present in
|
|
||||||
// the module (verified in the .ptx `.entry` list); `&mut f` is a live out-param.
|
|
||||||
unsafe {
|
|
||||||
ck(
|
|
||||||
cuModuleGetFunction(&mut f, module, name.as_ptr()),
|
|
||||||
"cuModuleGetFunction",
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(f)
|
|
||||||
};
|
|
||||||
let f_argb = getf(c"blend_argb")?;
|
|
||||||
let f_nv12 = getf(c"blend_nv12")?;
|
|
||||||
let f_yuv444 = getf(c"blend_yuv444")?;
|
|
||||||
let mut cur_buf: CUdeviceptr = 0;
|
|
||||||
// SAFETY: `&mut cur_buf` is a live out-param; the size fits the CURSOR_MAX² RGBA buffer.
|
|
||||||
unsafe {
|
|
||||||
ck(
|
|
||||||
cuMemAlloc_v2(&mut cur_buf, (CURSOR_MAX * CURSOR_MAX * 4) as usize),
|
|
||||||
"cuMemAlloc(cursor)",
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(CursorBlend {
|
|
||||||
module,
|
|
||||||
f_argb,
|
|
||||||
f_nv12,
|
|
||||||
f_yuv444,
|
|
||||||
cur_buf,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Upload the cursor RGBA (`cw*ch*4`, tight rows) into the device blend buffer. Call only when
|
|
||||||
/// the bitmap changes; position moves are just kernel args.
|
|
||||||
pub fn upload(&self, rgba: &[u8], cw: u32, ch: u32) -> Result<()> {
|
|
||||||
let cw = cw.min(CURSOR_MAX);
|
|
||||||
let ch = ch.min(CURSOR_MAX);
|
|
||||||
let row = cw as usize * 4;
|
|
||||||
let copy = CUDA_MEMCPY2D {
|
|
||||||
srcMemoryType: 1, // HOST
|
|
||||||
srcHost: rgba.as_ptr() as *const c_void,
|
|
||||||
srcPitch: row,
|
|
||||||
dstMemoryType: CU_MEMORYTYPE_DEVICE,
|
|
||||||
dstDevice: self.cur_buf,
|
|
||||||
dstPitch: row,
|
|
||||||
WidthInBytes: row,
|
|
||||||
Height: ch as usize,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
// SAFETY: HOST→DEVICE 2D copy of `row*ch` bytes; `rgba` covers at least that (caller passes
|
|
||||||
// `cw*ch*4`), `cur_buf` is the CURSOR_MAX²·4 device alloc (row ≤ CURSOR_MAX·4, ch ≤ CURSOR_MAX).
|
|
||||||
// Synchronous via `copy_blocking`. Requires the context current (caller's contract).
|
|
||||||
unsafe { copy_blocking(©, "cursor HtoD") }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Blend into a packed 4-byte (NVENC ARGB) owned surface at `(ox,oy)`.
|
|
||||||
#[allow(clippy::too_many_arguments)] // surface geometry + cursor size + offset — a struct would just be unpacked at the call
|
|
||||||
pub fn blend_argb(
|
|
||||||
&self,
|
|
||||||
surf: CUdeviceptr,
|
|
||||||
pitch: usize,
|
|
||||||
w: u32,
|
|
||||||
h: u32,
|
|
||||||
cw: u32,
|
|
||||||
ch: u32,
|
|
||||||
ox: i32,
|
|
||||||
oy: i32,
|
|
||||||
sync: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
let (mut a_surf, mut a_cur) = (surf, self.cur_buf);
|
|
||||||
let (mut a_pitch, mut a_w, mut a_h) = (pitch as i32, w as i32, h as i32);
|
|
||||||
let (mut a_cw, mut a_ch) = (cw.min(CURSOR_MAX) as i32, ch.min(CURSOR_MAX) as i32);
|
|
||||||
let (mut a_ox, mut a_oy) = (ox, oy);
|
|
||||||
let mut args: [*mut c_void; 9] = [
|
|
||||||
&mut a_surf as *mut _ as *mut c_void,
|
|
||||||
&mut a_pitch as *mut _ as *mut c_void,
|
|
||||||
&mut a_w as *mut _ as *mut c_void,
|
|
||||||
&mut a_h as *mut _ as *mut c_void,
|
|
||||||
&mut a_cur as *mut _ as *mut c_void,
|
|
||||||
&mut a_cw as *mut _ as *mut c_void,
|
|
||||||
&mut a_ch as *mut _ as *mut c_void,
|
|
||||||
&mut a_ox as *mut _ as *mut c_void,
|
|
||||||
&mut a_oy as *mut _ as *mut c_void,
|
|
||||||
];
|
|
||||||
self.launch(self.f_argb, a_cw as u32, a_ch as u32, &mut args, sync)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Blend into an owned planar YUV444 surface (3 stacked full-res planes) at `(ox,oy)`.
|
|
||||||
#[allow(clippy::too_many_arguments)] // surface geometry + cursor size + offset — a struct would just be unpacked at the call
|
|
||||||
pub fn blend_yuv444(
|
|
||||||
&self,
|
|
||||||
base: CUdeviceptr,
|
|
||||||
pitch: usize,
|
|
||||||
w: u32,
|
|
||||||
h: u32,
|
|
||||||
cw: u32,
|
|
||||||
ch: u32,
|
|
||||||
ox: i32,
|
|
||||||
oy: i32,
|
|
||||||
sync: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
let (mut a_base, mut a_cur) = (base, self.cur_buf);
|
|
||||||
let (mut a_pitch, mut a_w, mut a_h) = (pitch as i32, w as i32, h as i32);
|
|
||||||
let (mut a_cw, mut a_ch) = (cw.min(CURSOR_MAX) as i32, ch.min(CURSOR_MAX) as i32);
|
|
||||||
let (mut a_ox, mut a_oy) = (ox, oy);
|
|
||||||
let mut args: [*mut c_void; 9] = [
|
|
||||||
&mut a_base as *mut _ as *mut c_void,
|
|
||||||
&mut a_pitch as *mut _ as *mut c_void,
|
|
||||||
&mut a_w as *mut _ as *mut c_void,
|
|
||||||
&mut a_h as *mut _ as *mut c_void,
|
|
||||||
&mut a_cur as *mut _ as *mut c_void,
|
|
||||||
&mut a_cw as *mut _ as *mut c_void,
|
|
||||||
&mut a_ch as *mut _ as *mut c_void,
|
|
||||||
&mut a_ox as *mut _ as *mut c_void,
|
|
||||||
&mut a_oy as *mut _ as *mut c_void,
|
|
||||||
];
|
|
||||||
self.launch(self.f_yuv444, a_cw as u32, a_ch as u32, &mut args, sync)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Blend into an owned NV12 surface (Y plane at `base`, interleaved UV at `base + pitch*h`).
|
|
||||||
#[allow(clippy::too_many_arguments)] // surface geometry + cursor size + offset — a struct would just be unpacked at the call
|
|
||||||
pub fn blend_nv12(
|
|
||||||
&self,
|
|
||||||
base: CUdeviceptr,
|
|
||||||
pitch: usize,
|
|
||||||
w: u32,
|
|
||||||
h: u32,
|
|
||||||
cw: u32,
|
|
||||||
ch: u32,
|
|
||||||
ox: i32,
|
|
||||||
oy: i32,
|
|
||||||
sync: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
let (mut a_yb, mut a_uvb, mut a_cur) = (base, base + pitch as u64 * h as u64, self.cur_buf);
|
|
||||||
let (mut a_yp, mut a_uvp) = (pitch as i32, pitch as i32);
|
|
||||||
let (mut a_w, mut a_h) = (w as i32, h as i32);
|
|
||||||
let (mut a_cw, mut a_ch) = (cw.min(CURSOR_MAX) as i32, ch.min(CURSOR_MAX) as i32);
|
|
||||||
let (mut a_ox, mut a_oy) = (ox, oy);
|
|
||||||
let mut args: [*mut c_void; 11] = [
|
|
||||||
&mut a_yb as *mut _ as *mut c_void,
|
|
||||||
&mut a_yp as *mut _ as *mut c_void,
|
|
||||||
&mut a_uvb as *mut _ as *mut c_void,
|
|
||||||
&mut a_uvp as *mut _ as *mut c_void,
|
|
||||||
&mut a_w as *mut _ as *mut c_void,
|
|
||||||
&mut a_h as *mut _ as *mut c_void,
|
|
||||||
&mut a_cur as *mut _ as *mut c_void,
|
|
||||||
&mut a_cw as *mut _ as *mut c_void,
|
|
||||||
&mut a_ch as *mut _ as *mut c_void,
|
|
||||||
&mut a_ox as *mut _ as *mut c_void,
|
|
||||||
&mut a_oy as *mut _ as *mut c_void,
|
|
||||||
];
|
|
||||||
// One thread per 2x2 luma block → grid over ceil(cw/2) × ceil(ch/2).
|
|
||||||
self.launch(
|
|
||||||
self.f_nv12,
|
|
||||||
(a_cw as u32).div_ceil(2),
|
|
||||||
(a_ch as u32).div_ceil(2),
|
|
||||||
&mut args,
|
|
||||||
sync,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Launch `f` over a `work_w × work_h` grid (16×16 blocks) on the copy stream; `sync` waits
|
|
||||||
/// for it, `!sync` leaves completion to the stream (stream-ordered consumers only — the
|
|
||||||
/// kernel PARAMETERS are copied at launch time, so the arg locals need not outlive the call).
|
|
||||||
fn launch(
|
|
||||||
&self,
|
|
||||||
f: CUfunction,
|
|
||||||
work_w: u32,
|
|
||||||
work_h: u32,
|
|
||||||
args: &mut [*mut c_void],
|
|
||||||
sync: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
if work_w == 0 || work_h == 0 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
const B: u32 = 16;
|
|
||||||
let stream = copy_stream();
|
|
||||||
// SAFETY: `f` is a resolved kernel from our loaded module; `args` holds pointers to live
|
|
||||||
// locals whose types match the kernel's C parameters (per the call site above) — CUDA
|
|
||||||
// copies the parameter values during `cuLaunchKernel` itself, so they need not outlive
|
|
||||||
// the call. Grid/block dims are non-zero. Launched on the copy stream (ordered after the
|
|
||||||
// input-surface copy issued on the same stream); `sync` waits, `!sync` leaves ordering to
|
|
||||||
// the stream (the NVENC IO-stream binding). Requires the context current.
|
|
||||||
unsafe {
|
|
||||||
ck(
|
|
||||||
cuLaunchKernel(
|
|
||||||
f,
|
|
||||||
work_w.div_ceil(B),
|
|
||||||
work_h.div_ceil(B),
|
|
||||||
1,
|
|
||||||
B,
|
|
||||||
B,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
stream,
|
|
||||||
args.as_mut_ptr(),
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
),
|
|
||||||
"cuLaunchKernel(cursor)",
|
|
||||||
)?;
|
|
||||||
if sync {
|
|
||||||
ck(cuStreamSynchronize(stream), "cuStreamSynchronize(cursor)")?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for CursorBlend {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
// SAFETY: `cur_buf`/`module` are our own handles, freed exactly once here; the context is
|
|
||||||
// current on the encode thread that drops the encoder. Errors are ignored on teardown.
|
|
||||||
unsafe {
|
|
||||||
let _ = cuMemFree_v2(self.cur_buf);
|
|
||||||
let _ = cuModuleUnload(self.module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allocate one pitched device buffer for `width`x`height` 4-byte pixels; returns `(ptr, pitch)`.
|
/// Allocate one pitched device buffer for `width`x`height` 4-byte pixels; returns `(ptr, pitch)`.
|
||||||
fn alloc_pitched(width: u32, height: u32) -> Result<(CUdeviceptr, usize)> {
|
fn alloc_pitched(width: u32, height: u32) -> Result<(CUdeviceptr, usize)> {
|
||||||
let mut ptr: CUdeviceptr = 0;
|
let mut ptr: CUdeviceptr = 0;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use std::os::raw::{c_char, c_int, c_uint, c_void};
|
use std::os::raw::{c_int, c_uint, c_void};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
pub type CUresult = c_uint; // CUDA_SUCCESS == 0
|
pub type CUresult = c_uint; // CUDA_SUCCESS == 0
|
||||||
@@ -20,8 +20,6 @@ pub type CUdeviceptr = u64;
|
|||||||
pub type CUgraphicsResource = *mut c_void;
|
pub type CUgraphicsResource = *mut c_void;
|
||||||
pub type CUarray = *mut c_void;
|
pub type CUarray = *mut c_void;
|
||||||
pub type CUexternalMemory = *mut c_void; // opaque CUextMemory_st*
|
pub type CUexternalMemory = *mut c_void; // opaque CUextMemory_st*
|
||||||
pub type CUmodule = *mut c_void; // opaque CUmod_st*
|
|
||||||
pub type CUfunction = *mut c_void; // opaque CUfunc_st*
|
|
||||||
|
|
||||||
/// `CUmemorytype` (cuda.h): HOST=1, DEVICE=2, ARRAY=3, UNIFIED=4.
|
/// `CUmemorytype` (cuda.h): HOST=1, DEVICE=2, ARRAY=3, UNIFIED=4.
|
||||||
pub const CU_MEMORYTYPE_DEVICE: c_uint = 2;
|
pub const CU_MEMORYTYPE_DEVICE: c_uint = 2;
|
||||||
@@ -144,26 +142,6 @@ pub(crate) struct CudaApi {
|
|||||||
cuIpcGetMemHandle: unsafe extern "C" fn(*mut CUipcMemHandle, CUdeviceptr) -> CUresult,
|
cuIpcGetMemHandle: unsafe extern "C" fn(*mut CUipcMemHandle, CUdeviceptr) -> CUresult,
|
||||||
cuIpcOpenMemHandle: unsafe extern "C" fn(*mut CUdeviceptr, CUipcMemHandle, c_uint) -> CUresult,
|
cuIpcOpenMemHandle: unsafe extern "C" fn(*mut CUdeviceptr, CUipcMemHandle, c_uint) -> CUresult,
|
||||||
cuIpcCloseMemHandle: unsafe extern "C" fn(CUdeviceptr) -> CUresult,
|
cuIpcCloseMemHandle: unsafe extern "C" fn(CUdeviceptr) -> CUresult,
|
||||||
// Cursor-overlay blend: a linear device alloc + a PTX module with the blend kernels launched
|
|
||||||
// over the cursor's small rectangle (see [`CursorBlend`]).
|
|
||||||
cuMemAlloc_v2: unsafe extern "C" fn(*mut CUdeviceptr, usize) -> CUresult,
|
|
||||||
cuModuleLoadData: unsafe extern "C" fn(*mut CUmodule, *const c_void) -> CUresult,
|
|
||||||
cuModuleUnload: unsafe extern "C" fn(CUmodule) -> CUresult,
|
|
||||||
cuModuleGetFunction: unsafe extern "C" fn(*mut CUfunction, CUmodule, *const c_char) -> CUresult,
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
cuLaunchKernel: unsafe extern "C" fn(
|
|
||||||
CUfunction,
|
|
||||||
c_uint,
|
|
||||||
c_uint,
|
|
||||||
c_uint,
|
|
||||||
c_uint,
|
|
||||||
c_uint,
|
|
||||||
c_uint,
|
|
||||||
c_uint,
|
|
||||||
CUstream,
|
|
||||||
*mut *mut c_void,
|
|
||||||
*mut *mut c_void,
|
|
||||||
) -> CUresult,
|
|
||||||
}
|
}
|
||||||
// SAFETY: every field is a bare `extern "C" fn` address into the leaked, process-lifetime
|
// SAFETY: every field is a bare `extern "C" fn` address into the leaked, process-lifetime
|
||||||
// `libcuda` mapping (`cuda_api` `forget`s the `Library`, so it is never unloaded) — an immutable
|
// `libcuda` mapping (`cuda_api` `forget`s the `Library`, so it is never unloaded) — an immutable
|
||||||
@@ -236,11 +214,6 @@ pub(crate) fn cuda_api() -> Option<&'static CudaApi> {
|
|||||||
.or_else(|_| lib.get(b"cuIpcOpenMemHandle\0"))
|
.or_else(|_| lib.get(b"cuIpcOpenMemHandle\0"))
|
||||||
.ok()?,
|
.ok()?,
|
||||||
cuIpcCloseMemHandle: *lib.get(b"cuIpcCloseMemHandle\0").ok()?,
|
cuIpcCloseMemHandle: *lib.get(b"cuIpcCloseMemHandle\0").ok()?,
|
||||||
cuMemAlloc_v2: *lib.get(b"cuMemAlloc_v2\0").ok()?,
|
|
||||||
cuModuleLoadData: *lib.get(b"cuModuleLoadData\0").ok()?,
|
|
||||||
cuModuleUnload: *lib.get(b"cuModuleUnload\0").ok()?,
|
|
||||||
cuModuleGetFunction: *lib.get(b"cuModuleGetFunction\0").ok()?,
|
|
||||||
cuLaunchKernel: *lib.get(b"cuLaunchKernel\0").ok()?,
|
|
||||||
};
|
};
|
||||||
std::mem::forget(lib); // keep libcuda mapped for the fn pointers' lifetime (process)
|
std::mem::forget(lib); // keep libcuda mapped for the fn pointers' lifetime (process)
|
||||||
Some(api)
|
Some(api)
|
||||||
@@ -304,53 +277,6 @@ pub(crate) unsafe fn cuMemFree_v2(dptr: CUdeviceptr) -> CUresult {
|
|||||||
None => CU_ERROR_NOT_LOADED,
|
None => CU_ERROR_NOT_LOADED,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub(crate) unsafe fn cuMemAlloc_v2(dptr: *mut CUdeviceptr, size: usize) -> CUresult {
|
|
||||||
match cuda_api() {
|
|
||||||
Some(a) => (a.cuMemAlloc_v2)(dptr, size),
|
|
||||||
None => CU_ERROR_NOT_LOADED,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub(crate) unsafe fn cuModuleLoadData(m: *mut CUmodule, image: *const c_void) -> CUresult {
|
|
||||||
match cuda_api() {
|
|
||||||
Some(a) => (a.cuModuleLoadData)(m, image),
|
|
||||||
None => CU_ERROR_NOT_LOADED,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub(crate) unsafe fn cuModuleUnload(m: CUmodule) -> CUresult {
|
|
||||||
match cuda_api() {
|
|
||||||
Some(a) => (a.cuModuleUnload)(m),
|
|
||||||
None => CU_ERROR_NOT_LOADED,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub(crate) unsafe fn cuModuleGetFunction(
|
|
||||||
f: *mut CUfunction,
|
|
||||||
m: CUmodule,
|
|
||||||
name: *const c_char,
|
|
||||||
) -> CUresult {
|
|
||||||
match cuda_api() {
|
|
||||||
Some(a) => (a.cuModuleGetFunction)(f, m, name),
|
|
||||||
None => CU_ERROR_NOT_LOADED,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub(crate) unsafe fn cuLaunchKernel(
|
|
||||||
f: CUfunction,
|
|
||||||
gx: c_uint,
|
|
||||||
gy: c_uint,
|
|
||||||
gz: c_uint,
|
|
||||||
bx: c_uint,
|
|
||||||
by: c_uint,
|
|
||||||
bz: c_uint,
|
|
||||||
shmem: c_uint,
|
|
||||||
stream: CUstream,
|
|
||||||
params: *mut *mut c_void,
|
|
||||||
extra: *mut *mut c_void,
|
|
||||||
) -> CUresult {
|
|
||||||
match cuda_api() {
|
|
||||||
Some(a) => (a.cuLaunchKernel)(f, gx, gy, gz, bx, by, bz, shmem, stream, params, extra),
|
|
||||||
None => CU_ERROR_NOT_LOADED,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub(crate) unsafe fn cuMemcpy2DAsync_v2(copy: *const CUDA_MEMCPY2D, stream: CUstream) -> CUresult {
|
pub(crate) unsafe fn cuMemcpy2DAsync_v2(copy: *const CUDA_MEMCPY2D, stream: CUstream) -> CUresult {
|
||||||
match cuda_api() {
|
match cuda_api() {
|
||||||
Some(a) => (a.cuMemcpy2DAsync_v2)(copy, stream),
|
Some(a) => (a.cuMemcpy2DAsync_v2)(copy, stream),
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
#version 450
|
||||||
|
// Cursor-overlay blend for the direct-SDK NVENC path (cursor-as-metadata), dispatched over the
|
||||||
|
// cursor's rectangle only — the Vulkan replacement for the retired cursor_blend.cu PTX kernels
|
||||||
|
// (PTX is JIT'd against the driver's ISA ceiling, so a vendored blob silently dies on older
|
||||||
|
// drivers: CUDA errors 222/218 on-glass; SPIR-V has no such coupling). The NVENC input surface is
|
||||||
|
// Vulkan-allocated, CUDA-imported external memory (see vkslot.rs), so this shader writes the very
|
||||||
|
// bytes NVENC encodes.
|
||||||
|
//
|
||||||
|
// MODE (spec constant): 0 = packed 4-byte ARGB (NVENC byte order B,G,R,A), 1 = NV12 (Y plane +
|
||||||
|
// interleaved half-res UV at row surfH), 2 = planar YUV444 (3 full-res planes stacked at
|
||||||
|
// pitch*surfH). BT.709 limited-range coefficients — identical to rgb2nv12_buf.comp and the
|
||||||
|
// retired .cu, so the cursor colour matches the frame regardless of backend.
|
||||||
|
//
|
||||||
|
// The surface SSBO is uint[] (no 8-bit storage dependency — maximum driver reach): every
|
||||||
|
// invocation exclusively owns the 32-bit words it read-modify-writes. ARGB: one invocation per
|
||||||
|
// cursor pixel = one word. NV12/YUV444: one invocation per WORD-ALIGNED 4-px luma span (per two
|
||||||
|
// rows for NV12, whose 2 chroma bytes-pairs land in one exclusive word). Spans are aligned to the
|
||||||
|
// SURFACE, not the cursor, so neighbouring invocations never share a word even at odd `ox`.
|
||||||
|
//
|
||||||
|
// Rebuild: glslc cursor_blend.comp -o cursor_blend.spv (vendored beside this file)
|
||||||
|
|
||||||
|
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
|
||||||
|
|
||||||
|
layout(constant_id = 0) const uint MODE = 0;
|
||||||
|
|
||||||
|
layout(std430, binding = 0) buffer Surf { uint surf[]; };
|
||||||
|
layout(std430, binding = 1) readonly buffer Cur { uint cur[]; };
|
||||||
|
|
||||||
|
layout(push_constant) uniform Push {
|
||||||
|
uint pitch; // surface row stride, bytes (4-aligned by construction)
|
||||||
|
uint surfW; // content width, px
|
||||||
|
uint surfH; // luma rows (plane stride multiplier)
|
||||||
|
uint curW; // cursor bitmap width, px
|
||||||
|
uint curH; // cursor bitmap height, px
|
||||||
|
int ox; // cursor top-left on the surface, px (may be negative)
|
||||||
|
int oy;
|
||||||
|
} pc;
|
||||||
|
|
||||||
|
// Cursor texel (straight-alpha RGBA, tight rows) or (0,0,0,0) outside the bitmap.
|
||||||
|
uvec4 cursor_px(int cx, int cy) {
|
||||||
|
if (cx < 0 || cy < 0 || cx >= int(pc.curW) || cy >= int(pc.curH)) return uvec4(0);
|
||||||
|
uint w = cur[uint(cy) * pc.curW + uint(cx)];
|
||||||
|
return uvec4(w & 0xFFu, (w >> 8) & 0xFFu, (w >> 16) & 0xFFu, (w >> 24) & 0xFFu); // R,G,B,A
|
||||||
|
}
|
||||||
|
|
||||||
|
uint blend8(uint dst, uint src, uint a) {
|
||||||
|
return (src * a + dst * (255u - a)) / 255u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BT.709 limited RGB→Y/U/V (matches the retired .cu / rgb2nv12_buf.comp).
|
||||||
|
uint y_of(uvec4 s) {
|
||||||
|
return uint(clamp(16.0 + 0.1826 * float(s.r) + 0.6142 * float(s.g) + 0.0620 * float(s.b) + 0.5, 0.0, 255.0));
|
||||||
|
}
|
||||||
|
float u_of(uvec4 s) { return 128.0 - 0.1006 * float(s.r) - 0.3386 * float(s.g) + 0.4392 * float(s.b); }
|
||||||
|
float v_of(uvec4 s) { return 128.0 + 0.4392 * float(s.r) - 0.3989 * float(s.g) - 0.0403 * float(s.b); }
|
||||||
|
|
||||||
|
// Read-modify-write one byte lane of a word index.
|
||||||
|
void rmw_byte(uint word_idx, uint lane, uint val8, uint a) {
|
||||||
|
uint w = surf[word_idx];
|
||||||
|
uint shift = lane * 8u;
|
||||||
|
uint d = (w >> shift) & 0xFFu;
|
||||||
|
uint b = blend8(d, val8, a);
|
||||||
|
surf[word_idx] = (w & ~(0xFFu << shift)) | (b << shift);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
if (MODE == 0u) {
|
||||||
|
// ARGB: one invocation per cursor pixel; each surface pixel is one exclusive word.
|
||||||
|
int cx = int(gl_GlobalInvocationID.x);
|
||||||
|
int cy = int(gl_GlobalInvocationID.y);
|
||||||
|
if (cx >= int(pc.curW) || cy >= int(pc.curH)) return;
|
||||||
|
int px = pc.ox + cx, py = pc.oy + cy;
|
||||||
|
if (px < 0 || py < 0 || px >= int(pc.surfW) || py >= int(pc.surfH)) return;
|
||||||
|
uvec4 s = cursor_px(cx, cy);
|
||||||
|
if (s.a == 0u) return;
|
||||||
|
uint idx = (uint(py) * pc.pitch + uint(px) * 4u) / 4u;
|
||||||
|
uint w = surf[idx];
|
||||||
|
uint b = blend8(w & 0xFFu, s.b, s.a); // B lane
|
||||||
|
uint g = blend8((w >> 8) & 0xFFu, s.g, s.a); // G lane
|
||||||
|
uint r = blend8((w >> 16) & 0xFFu, s.r, s.a); // R lane
|
||||||
|
surf[idx] = (w & 0xFF000000u) | (r << 16) | (g << 8) | b;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NV12 / YUV444: one invocation per SURFACE-word-aligned 4-px luma span. Span origin:
|
||||||
|
// x0 = floor(ox/4)*4 + span*4 (surface px), rows walk the cursor rect.
|
||||||
|
int span = int(gl_GlobalInvocationID.x);
|
||||||
|
int row = int(gl_GlobalInvocationID.y);
|
||||||
|
int x0 = (pc.ox >> 2) << 2; // word-aligned start at/left-of ox (ox may be negative)
|
||||||
|
int px0 = x0 + span * 4;
|
||||||
|
|
||||||
|
if (MODE == 2u) {
|
||||||
|
// YUV444: rows walk cursor rows one at a time.
|
||||||
|
if (row >= int(pc.curH)) return;
|
||||||
|
int py = pc.oy + row;
|
||||||
|
if (py < 0 || py >= int(pc.surfH)) return;
|
||||||
|
uint plane = pc.pitch * pc.surfH;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
int px = px0 + i;
|
||||||
|
int cx = px - pc.ox;
|
||||||
|
if (px < 0 || px >= int(pc.surfW)) continue;
|
||||||
|
uvec4 s = cursor_px(cx, row);
|
||||||
|
if (s.a == 0u) continue;
|
||||||
|
uint off = uint(py) * pc.pitch + uint(px);
|
||||||
|
uint U = uint(clamp(u_of(s) + 0.5, 0.0, 255.0));
|
||||||
|
uint V = uint(clamp(v_of(s) + 0.5, 0.0, 255.0));
|
||||||
|
rmw_byte(off / 4u, off % 4u, y_of(s), s.a);
|
||||||
|
rmw_byte((plane + off) / 4u, (plane + off) % 4u, U, s.a);
|
||||||
|
rmw_byte((2u * plane + off) / 4u, (2u * plane + off) % 4u, V, s.a);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NV12: rows walk 2-row luma blocks (row = block row). The span's 4 luma px × 2 rows are
|
||||||
|
// exclusive words; its 2 chroma samples (4 bytes) are one exclusive word.
|
||||||
|
int base_cy = row * 2;
|
||||||
|
if (base_cy >= int(pc.curH)) return;
|
||||||
|
// Luma: 4 px × 2 rows.
|
||||||
|
for (int j = 0; j < 2; j++) {
|
||||||
|
int cy = base_cy + j;
|
||||||
|
int py = pc.oy + cy;
|
||||||
|
if (cy >= int(pc.curH) || py < 0 || py >= int(pc.surfH)) continue;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
int px = px0 + i;
|
||||||
|
int cx = px - pc.ox;
|
||||||
|
if (px < 0 || px >= int(pc.surfW)) continue;
|
||||||
|
uvec4 s = cursor_px(cx, cy);
|
||||||
|
if (s.a == 0u) continue;
|
||||||
|
uint off = uint(py) * pc.pitch + uint(px);
|
||||||
|
rmw_byte(off / 4u, off % 4u, y_of(s), s.a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Chroma: two UV samples covering the span's 2x2 blocks, alpha-weighted like the .cu kernel.
|
||||||
|
// The UV plane starts at row surfH; sample (uvx, uvy) lives at uv_base + uvy*pitch + uvx*2.
|
||||||
|
// Guard: only spans whose px0 is 4-aligned own their chroma word (px0 is by construction).
|
||||||
|
int py_top = pc.oy + base_cy;
|
||||||
|
int uvy = py_top >> 1;
|
||||||
|
if (py_top < 0 || uvy < 0 || uvy * 2 >= int(pc.surfH)) return;
|
||||||
|
uint uv_base = pc.pitch * pc.surfH;
|
||||||
|
for (int hf = 0; hf < 2; hf++) {
|
||||||
|
// Each hf = one 2x2 luma block = one UV sample (2 bytes).
|
||||||
|
int bx = px0 + hf * 2;
|
||||||
|
if (bx < 0 || bx >= int(pc.surfW)) continue;
|
||||||
|
int uvx = bx >> 1;
|
||||||
|
float ua = 0.0, va = 0.0, wa = 0.0;
|
||||||
|
int cnt = 0;
|
||||||
|
for (int j = 0; j < 2; j++) {
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
int px = bx + i;
|
||||||
|
int py = py_top + j;
|
||||||
|
int cx = px - pc.ox;
|
||||||
|
int cy = base_cy + j;
|
||||||
|
if (px < 0 || py < 0 || px >= int(pc.surfW) || py >= int(pc.surfH)) continue;
|
||||||
|
uvec4 s = cursor_px(cx, cy);
|
||||||
|
if (s.a == 0u) continue;
|
||||||
|
ua += u_of(s) * float(s.a);
|
||||||
|
va += v_of(s) * float(s.a);
|
||||||
|
wa += float(s.a);
|
||||||
|
cnt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (wa <= 0.0 || cnt == 0) continue;
|
||||||
|
uint U = uint(clamp(ua / wa + 0.5, 0.0, 255.0));
|
||||||
|
uint V = uint(clamp(va / wa + 0.5, 0.0, 255.0));
|
||||||
|
uint amean = uint(clamp(wa / float(cnt) + 0.5, 0.0, 255.0));
|
||||||
|
uint off = uv_base + uint(uvy) * pc.pitch + uint(uvx) * 2u;
|
||||||
|
rmw_byte(off / 4u, off % 4u, U, amean);
|
||||||
|
rmw_byte((off + 1u) / 4u, (off + 1u) % 4u, V, amean);
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -15,6 +15,7 @@ pub mod client;
|
|||||||
pub mod cuda;
|
pub mod cuda;
|
||||||
pub mod egl;
|
pub mod egl;
|
||||||
pub mod proto;
|
pub mod proto;
|
||||||
|
pub mod vkslot;
|
||||||
pub mod vulkan;
|
pub mod vulkan;
|
||||||
pub mod worker;
|
pub mod worker;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,710 @@
|
|||||||
|
//! Vulkan-allocated NVENC input slots + the Vulkan compute cursor blend — the driver-portable
|
||||||
|
//! replacement for the retired `cursor_blend.cu` PTX kernels (design: remote-desktop-sweep §8,
|
||||||
|
//! Phase A). A vendored PTX blob is JIT'd against the driver's ISA ceiling, so it silently dies
|
||||||
|
//! on drivers older than the generating toolkit (CUDA errors 222/218 on-glass — the KWin leg's
|
||||||
|
//! invisible composite cursor); SPIR-V has no such coupling.
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! exportable VkBuffer ──vkGetMemoryFdKHR(OPAQUE_FD)──▶ cuImportExternalMemory ──▶ CUdeviceptr
|
||||||
|
//! ▲ │ NVENC registers + encodes
|
||||||
|
//! └── cursor_blend.comp dispatch (cursor rect only) ◀───┘ CUDA copies frames in
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The direct-SDK NVENC encoder allocates its input ring through [`VkSlotBlend::alloc_slot`]
|
||||||
|
//! instead of `cuMemAllocPitch`: same contiguous layouts (`InputSurface` docs), but the memory is
|
||||||
|
//! Vulkan external memory both APIs address. Per cursor-bearing frame the encoder CPU-syncs its
|
||||||
|
//! CUDA copy, then [`VkSlotBlend::blend`] dispatches the compute blend over the cursor's
|
||||||
|
//! rectangle and fence-waits — the same coherence ceremony [`super::vulkan::VkBridge`] ships for
|
||||||
|
//! its CSC (fence-ordered cross-API access on NVIDIA, no queue-family transfer needed). Frames
|
||||||
|
//! without a cursor never touch Vulkan, keeping the stream-ordered fast path intact.
|
||||||
|
//!
|
||||||
|
//! Falls back cleanly: if bring-up fails the encoder allocates plain CUDA surfaces and composite
|
||||||
|
//! mode degrades to no cursor (warned once) — never a failed session.
|
||||||
|
|
||||||
|
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||||
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
|
use super::cuda::{self, CUdeviceptr};
|
||||||
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
|
use ash::vk;
|
||||||
|
|
||||||
|
/// Max cursor-overlay bitmap edge (px) — matches [`cuda::CURSOR_MAX`] and the capture-side clamp.
|
||||||
|
pub const CURSOR_MAX: u32 = cuda::CURSOR_MAX;
|
||||||
|
|
||||||
|
/// The vendored SPIR-V for `cursor_blend.comp` (beside this file; rebuild with
|
||||||
|
/// `glslc cursor_blend.comp -o cursor_blend.spv`).
|
||||||
|
const CURSOR_SPV: &[u8] = include_bytes!("cursor_blend.spv");
|
||||||
|
|
||||||
|
/// NVENC input-surface layout — selects the spec-constant `MODE` pipeline and the allocation
|
||||||
|
/// arithmetic (mirroring `InputSurface`'s contiguous layouts).
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum SlotFormat {
|
||||||
|
/// Packed 4-byte ARGB (NVENC byte order B,G,R,A): `pitch × height`.
|
||||||
|
Argb,
|
||||||
|
/// NV12: Y rows `[0, H)` + interleaved UV rows `[H, 3H/2)` under one pitch.
|
||||||
|
Nv12,
|
||||||
|
/// Planar YUV444: three full-res planes stacked at `pitch × height` intervals.
|
||||||
|
Yuv444,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlotFormat {
|
||||||
|
fn mode(self) -> u32 {
|
||||||
|
match self {
|
||||||
|
SlotFormat::Argb => 0,
|
||||||
|
SlotFormat::Nv12 => 1,
|
||||||
|
SlotFormat::Yuv444 => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn row_bytes(self, width: u32) -> u64 {
|
||||||
|
match self {
|
||||||
|
SlotFormat::Argb => width as u64 * 4,
|
||||||
|
SlotFormat::Nv12 | SlotFormat::Yuv444 => width as u64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn rows(self, height: u32) -> u64 {
|
||||||
|
match self {
|
||||||
|
SlotFormat::Argb => height as u64,
|
||||||
|
SlotFormat::Nv12 => height as u64 + (height as u64 / 2).max(1),
|
||||||
|
SlotFormat::Yuv444 => height as u64 * 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the encoder holds per ring slot: the CUDA view it registers with NVENC plus the id it
|
||||||
|
/// hands back to [`VkSlotBlend::blend`]. The backing Vulkan objects + CUDA mapping live in the
|
||||||
|
/// [`VkSlotBlend`] (freed by [`free_slots`](VkSlotBlend::free_slots) / drop), so this is Copy —
|
||||||
|
/// the encoder's ring keeps its existing shape.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct VkSlotRef {
|
||||||
|
/// Device pointer NVENC registers (CUDA's mapping of the Vulkan memory).
|
||||||
|
pub ptr: CUdeviceptr,
|
||||||
|
/// Row stride in bytes (ours: row bytes rounded up to 256).
|
||||||
|
pub pitch: usize,
|
||||||
|
/// Luma rows (the plane-stride multiplier, as in `InputSurface`).
|
||||||
|
pub height: u32,
|
||||||
|
/// Index into the blend's slot table.
|
||||||
|
pub id: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One allocated slot's backing objects, freed together in reverse order (CUDA mapping first).
|
||||||
|
struct SlotAlloc {
|
||||||
|
buffer: vk::Buffer,
|
||||||
|
memory: vk::DeviceMemory,
|
||||||
|
/// CUDA's import of the exported OPAQUE_FD — must drop BEFORE the Vulkan memory is freed.
|
||||||
|
cuda: cuda::ExternalDmabuf,
|
||||||
|
size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 28-byte push-constant block matching `cursor_blend.comp`'s `Push`.
|
||||||
|
#[repr(C)]
|
||||||
|
struct Push {
|
||||||
|
pitch: u32,
|
||||||
|
surf_w: u32,
|
||||||
|
surf_h: u32,
|
||||||
|
cur_w: u32,
|
||||||
|
cur_h: u32,
|
||||||
|
ox: i32,
|
||||||
|
oy: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct VkSlotBlend {
|
||||||
|
_entry: ash::Entry,
|
||||||
|
instance: ash::Instance,
|
||||||
|
device: ash::Device,
|
||||||
|
ext_fd: ash::khr::external_memory_fd::Device,
|
||||||
|
queue: vk::Queue,
|
||||||
|
cmd_pool: vk::CommandPool,
|
||||||
|
cmd: vk::CommandBuffer,
|
||||||
|
fence: vk::Fence,
|
||||||
|
mem_props: vk::PhysicalDeviceMemoryProperties,
|
||||||
|
shader: vk::ShaderModule,
|
||||||
|
desc_layout: vk::DescriptorSetLayout,
|
||||||
|
pipe_layout: vk::PipelineLayout,
|
||||||
|
desc_pool: vk::DescriptorPool,
|
||||||
|
desc_set: vk::DescriptorSet,
|
||||||
|
/// One pipeline per [`SlotFormat`], indexed by `mode()` (spec constant).
|
||||||
|
pipelines: [vk::Pipeline; 3],
|
||||||
|
/// Host-visible cursor bitmap staging (CURSOR_MAX²·4, tight rows), persistently mapped.
|
||||||
|
cur_buf: vk::Buffer,
|
||||||
|
cur_mem: vk::DeviceMemory,
|
||||||
|
cur_map: *mut u8,
|
||||||
|
slots: Vec<SlotAlloc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: raw Vulkan handles + a persistently-mapped pointer, all uniquely owned by this struct
|
||||||
|
// and destroyed exactly once in `Drop`; used from the encoder thread but moved with it. `Send`
|
||||||
|
// only (not `Sync`), matching the single-thread use — transferring opaque handles cannot dangle.
|
||||||
|
unsafe impl Send for VkSlotBlend {}
|
||||||
|
|
||||||
|
impl VkSlotBlend {
|
||||||
|
/// Bring up the device + blend pipelines. Requires the CUDA shared context (the encoder's) to
|
||||||
|
/// be established; picks the NVIDIA physical device (the NVENC path is NVIDIA by definition).
|
||||||
|
pub fn new() -> Result<VkSlotBlend> {
|
||||||
|
// SAFETY: standard ash bring-up, same shape as `VkBridge::new` — every call is `unsafe`
|
||||||
|
// only because ash cannot statically verify handle/CreateInfo validity. Every
|
||||||
|
// `*CreateInfo`/`AllocateInfo` is built from locals that live for the duration of the
|
||||||
|
// synchronous call reading them; every handle passed was created and `?`-checked in this
|
||||||
|
// same function. Shares nothing across threads.
|
||||||
|
unsafe {
|
||||||
|
let entry = ash::Entry::load().context("load libvulkan")?;
|
||||||
|
let app = vk::ApplicationInfo::default().api_version(vk::API_VERSION_1_1);
|
||||||
|
let instance = entry
|
||||||
|
.create_instance(
|
||||||
|
&vk::InstanceCreateInfo::default().application_info(&app),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.context("vkCreateInstance")?;
|
||||||
|
let phys = match instance
|
||||||
|
.enumerate_physical_devices()
|
||||||
|
.context("enumerate GPUs")?
|
||||||
|
.into_iter()
|
||||||
|
.find(|&p| instance.get_physical_device_properties(p).vendor_id == 0x10DE)
|
||||||
|
{
|
||||||
|
Some(p) => p,
|
||||||
|
None => {
|
||||||
|
instance.destroy_instance(None);
|
||||||
|
return Err(anyhow!("no NVIDIA Vulkan device"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mem_props = instance.get_physical_device_memory_properties(phys);
|
||||||
|
let qf = match instance
|
||||||
|
.get_physical_device_queue_family_properties(phys)
|
||||||
|
.iter()
|
||||||
|
.position(|q| q.queue_flags.contains(vk::QueueFlags::COMPUTE))
|
||||||
|
{
|
||||||
|
Some(i) => i as u32,
|
||||||
|
None => {
|
||||||
|
instance.destroy_instance(None);
|
||||||
|
return Err(anyhow!("no compute-capable queue family"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let prio = [1.0f32];
|
||||||
|
let qci = [vk::DeviceQueueCreateInfo::default()
|
||||||
|
.queue_family_index(qf)
|
||||||
|
.queue_priorities(&prio)];
|
||||||
|
let exts = [ash::khr::external_memory_fd::NAME.as_ptr()];
|
||||||
|
let device = match instance.create_device(
|
||||||
|
phys,
|
||||||
|
&vk::DeviceCreateInfo::default()
|
||||||
|
.queue_create_infos(&qci)
|
||||||
|
.enabled_extension_names(&exts),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => {
|
||||||
|
instance.destroy_instance(None);
|
||||||
|
return Err(e).context("vkCreateDevice (external_memory_fd supported?)");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// From here teardown-on-error goes through `destroy_partial`, which tolerates null
|
||||||
|
// handles — build everything into an incrementally-filled struct.
|
||||||
|
let ext_fd = ash::khr::external_memory_fd::Device::new(&instance, &device);
|
||||||
|
let queue = device.get_device_queue(qf, 0);
|
||||||
|
let mut me = VkSlotBlend {
|
||||||
|
_entry: entry,
|
||||||
|
instance,
|
||||||
|
device,
|
||||||
|
ext_fd,
|
||||||
|
queue,
|
||||||
|
cmd_pool: vk::CommandPool::null(),
|
||||||
|
cmd: vk::CommandBuffer::null(),
|
||||||
|
fence: vk::Fence::null(),
|
||||||
|
mem_props,
|
||||||
|
shader: vk::ShaderModule::null(),
|
||||||
|
desc_layout: vk::DescriptorSetLayout::null(),
|
||||||
|
pipe_layout: vk::PipelineLayout::null(),
|
||||||
|
desc_pool: vk::DescriptorPool::null(),
|
||||||
|
desc_set: vk::DescriptorSet::null(),
|
||||||
|
pipelines: [vk::Pipeline::null(); 3],
|
||||||
|
cur_buf: vk::Buffer::null(),
|
||||||
|
cur_mem: vk::DeviceMemory::null(),
|
||||||
|
cur_map: std::ptr::null_mut(),
|
||||||
|
slots: Vec::new(),
|
||||||
|
};
|
||||||
|
me.init_objects(qf).inspect_err(|_| {
|
||||||
|
// `Drop` runs the same teardown and tolerates the nulls left by a partial init.
|
||||||
|
})?;
|
||||||
|
tracing::info!(
|
||||||
|
"Vulkan slot blend ready (exportable NVENC inputs + SPIR-V cursor blend)"
|
||||||
|
);
|
||||||
|
Ok(me)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The non-device objects: command machinery, cursor staging, descriptor + pipelines.
|
||||||
|
fn init_objects(&mut self, qf: u32) -> Result<()> {
|
||||||
|
// SAFETY: same contract as `new` — ash calls on the live `self.device` with builder infos
|
||||||
|
// from locals outliving each synchronous call; created handles are stored into `self`
|
||||||
|
// immediately so the caller's `Drop` frees them on any later failure.
|
||||||
|
unsafe {
|
||||||
|
let d = &self.device;
|
||||||
|
self.cmd_pool = d
|
||||||
|
.create_command_pool(
|
||||||
|
&vk::CommandPoolCreateInfo::default()
|
||||||
|
.queue_family_index(qf)
|
||||||
|
.flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.context("create command pool")?;
|
||||||
|
self.cmd = d
|
||||||
|
.allocate_command_buffers(
|
||||||
|
&vk::CommandBufferAllocateInfo::default()
|
||||||
|
.command_pool(self.cmd_pool)
|
||||||
|
.level(vk::CommandBufferLevel::PRIMARY)
|
||||||
|
.command_buffer_count(1),
|
||||||
|
)
|
||||||
|
.context("allocate command buffer")?[0];
|
||||||
|
self.fence = d
|
||||||
|
.create_fence(&vk::FenceCreateInfo::default(), None)
|
||||||
|
.context("create fence")?;
|
||||||
|
|
||||||
|
// Cursor staging: host-visible+coherent SSBO, persistently mapped.
|
||||||
|
let cur_size = (CURSOR_MAX * CURSOR_MAX * 4) as u64;
|
||||||
|
self.cur_buf = d
|
||||||
|
.create_buffer(
|
||||||
|
&vk::BufferCreateInfo::default()
|
||||||
|
.size(cur_size)
|
||||||
|
.usage(vk::BufferUsageFlags::STORAGE_BUFFER),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.context("create cursor buffer")?;
|
||||||
|
let reqs = d.get_buffer_memory_requirements(self.cur_buf);
|
||||||
|
let mem_type = self
|
||||||
|
.memory_type(
|
||||||
|
reqs.memory_type_bits,
|
||||||
|
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
|
||||||
|
)
|
||||||
|
.context("cursor buffer memory type")?;
|
||||||
|
self.cur_mem = d
|
||||||
|
.allocate_memory(
|
||||||
|
&vk::MemoryAllocateInfo::default()
|
||||||
|
.allocation_size(reqs.size)
|
||||||
|
.memory_type_index(mem_type),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.context("allocate cursor memory")?;
|
||||||
|
d.bind_buffer_memory(self.cur_buf, self.cur_mem, 0)
|
||||||
|
.context("bind cursor memory")?;
|
||||||
|
self.cur_map = d
|
||||||
|
.map_memory(self.cur_mem, 0, cur_size, vk::MemoryMapFlags::empty())
|
||||||
|
.context("map cursor memory")? as *mut u8;
|
||||||
|
|
||||||
|
// Descriptor set: binding 0 = surface SSBO (rebound per blend), 1 = cursor SSBO.
|
||||||
|
let bindings = [
|
||||||
|
vk::DescriptorSetLayoutBinding::default()
|
||||||
|
.binding(0)
|
||||||
|
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
|
||||||
|
.descriptor_count(1)
|
||||||
|
.stage_flags(vk::ShaderStageFlags::COMPUTE),
|
||||||
|
vk::DescriptorSetLayoutBinding::default()
|
||||||
|
.binding(1)
|
||||||
|
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
|
||||||
|
.descriptor_count(1)
|
||||||
|
.stage_flags(vk::ShaderStageFlags::COMPUTE),
|
||||||
|
];
|
||||||
|
self.desc_layout = d
|
||||||
|
.create_descriptor_set_layout(
|
||||||
|
&vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.context("create descriptor layout")?;
|
||||||
|
let pc = [vk::PushConstantRange::default()
|
||||||
|
.stage_flags(vk::ShaderStageFlags::COMPUTE)
|
||||||
|
.size(std::mem::size_of::<Push>() as u32)];
|
||||||
|
let dl = [self.desc_layout];
|
||||||
|
self.pipe_layout = d
|
||||||
|
.create_pipeline_layout(
|
||||||
|
&vk::PipelineLayoutCreateInfo::default()
|
||||||
|
.set_layouts(&dl)
|
||||||
|
.push_constant_ranges(&pc),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.context("create pipeline layout")?;
|
||||||
|
let pool_sizes = [vk::DescriptorPoolSize::default()
|
||||||
|
.ty(vk::DescriptorType::STORAGE_BUFFER)
|
||||||
|
.descriptor_count(2)];
|
||||||
|
self.desc_pool = d
|
||||||
|
.create_descriptor_pool(
|
||||||
|
&vk::DescriptorPoolCreateInfo::default()
|
||||||
|
.max_sets(1)
|
||||||
|
.pool_sizes(&pool_sizes),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.context("create descriptor pool")?;
|
||||||
|
let dls = [self.desc_layout];
|
||||||
|
self.desc_set = d
|
||||||
|
.allocate_descriptor_sets(
|
||||||
|
&vk::DescriptorSetAllocateInfo::default()
|
||||||
|
.descriptor_pool(self.desc_pool)
|
||||||
|
.set_layouts(&dls),
|
||||||
|
)
|
||||||
|
.context("allocate descriptor set")?[0];
|
||||||
|
|
||||||
|
// The shader + one pipeline per MODE (spec constant 0).
|
||||||
|
if CURSOR_SPV.len() % 4 != 0 {
|
||||||
|
anyhow::bail!("cursor_blend.spv is not word-aligned");
|
||||||
|
}
|
||||||
|
let words: Vec<u32> = CURSOR_SPV
|
||||||
|
.chunks_exact(4)
|
||||||
|
.map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
|
||||||
|
.collect();
|
||||||
|
self.shader = d
|
||||||
|
.create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&words), None)
|
||||||
|
.context("create blend shader module")?;
|
||||||
|
for mode in 0u32..3 {
|
||||||
|
let entries = [vk::SpecializationMapEntry::default()
|
||||||
|
.constant_id(0)
|
||||||
|
.offset(0)
|
||||||
|
.size(4)];
|
||||||
|
let data = mode.to_le_bytes();
|
||||||
|
let spec = vk::SpecializationInfo::default()
|
||||||
|
.map_entries(&entries)
|
||||||
|
.data(&data);
|
||||||
|
let stage = vk::PipelineShaderStageCreateInfo::default()
|
||||||
|
.stage(vk::ShaderStageFlags::COMPUTE)
|
||||||
|
.module(self.shader)
|
||||||
|
.name(c"main")
|
||||||
|
.specialization_info(&spec);
|
||||||
|
let info = [vk::ComputePipelineCreateInfo::default()
|
||||||
|
.stage(stage)
|
||||||
|
.layout(self.pipe_layout)];
|
||||||
|
let p = d
|
||||||
|
.create_compute_pipelines(vk::PipelineCache::null(), &info, None)
|
||||||
|
.map_err(|(_, e)| e)
|
||||||
|
.context("create blend pipeline")?[0];
|
||||||
|
self.pipelines[mode as usize] = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_type(&self, type_bits: u32, flags: vk::MemoryPropertyFlags) -> Result<u32> {
|
||||||
|
(0..self.mem_props.memory_type_count)
|
||||||
|
.find(|&i| {
|
||||||
|
type_bits & (1 << i) != 0
|
||||||
|
&& self.mem_props.memory_types[i as usize]
|
||||||
|
.property_flags
|
||||||
|
.contains(flags)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| anyhow!("no memory type for flags {flags:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocate one NVENC input slot as exportable Vulkan memory mapped into CUDA. Layout matches
|
||||||
|
/// `InputSurface` (contiguous planes under one pitch); pitch = row bytes rounded to 256.
|
||||||
|
pub fn alloc_slot(&mut self, fmt: SlotFormat, width: u32, height: u32) -> Result<VkSlotRef> {
|
||||||
|
let pitch = (fmt.row_bytes(width) + 255) & !255;
|
||||||
|
let size = pitch * fmt.rows(height);
|
||||||
|
// SAFETY: exportable-buffer allocation, the exact `VkBridge::ensure_dst` incantation:
|
||||||
|
// `ExternalMemoryBufferCreateInfo`/`ExportMemoryAllocateInfo` declare OPAQUE_FD,
|
||||||
|
// `MemoryDedicatedAllocateInfo` ties the memory to the buffer; every info is a local
|
||||||
|
// outliving its synchronous call and every failure path destroys the objects created so
|
||||||
|
// far exactly once. `get_memory_fd` hands us an fd that `import_owned_fd` either adopts
|
||||||
|
// (driver owns it) or closes on failure.
|
||||||
|
unsafe {
|
||||||
|
let d = &self.device;
|
||||||
|
let mut ext_info = vk::ExternalMemoryBufferCreateInfo::default()
|
||||||
|
.handle_types(vk::ExternalMemoryHandleTypeFlags::OPAQUE_FD);
|
||||||
|
let buffer = d
|
||||||
|
.create_buffer(
|
||||||
|
&vk::BufferCreateInfo::default()
|
||||||
|
.size(size)
|
||||||
|
.usage(vk::BufferUsageFlags::STORAGE_BUFFER)
|
||||||
|
.push_next(&mut ext_info),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.context("create slot buffer")?;
|
||||||
|
let reqs = d.get_buffer_memory_requirements(buffer);
|
||||||
|
let mem_type = match self
|
||||||
|
.memory_type(reqs.memory_type_bits, vk::MemoryPropertyFlags::DEVICE_LOCAL)
|
||||||
|
{
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
d.destroy_buffer(buffer, None);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut export = vk::ExportMemoryAllocateInfo::default()
|
||||||
|
.handle_types(vk::ExternalMemoryHandleTypeFlags::OPAQUE_FD);
|
||||||
|
let mut dedicated = vk::MemoryDedicatedAllocateInfo::default().buffer(buffer);
|
||||||
|
let memory = match d.allocate_memory(
|
||||||
|
&vk::MemoryAllocateInfo::default()
|
||||||
|
.allocation_size(reqs.size)
|
||||||
|
.memory_type_index(mem_type)
|
||||||
|
.push_next(&mut export)
|
||||||
|
.push_next(&mut dedicated),
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
d.destroy_buffer(buffer, None);
|
||||||
|
return Err(e).context("allocate exportable slot memory");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = d.bind_buffer_memory(buffer, memory, 0) {
|
||||||
|
d.free_memory(memory, None);
|
||||||
|
d.destroy_buffer(buffer, None);
|
||||||
|
return Err(e).context("bind slot memory");
|
||||||
|
}
|
||||||
|
let fd = match self.ext_fd.get_memory_fd(
|
||||||
|
&vk::MemoryGetFdInfoKHR::default()
|
||||||
|
.memory(memory)
|
||||||
|
.handle_type(vk::ExternalMemoryHandleTypeFlags::OPAQUE_FD),
|
||||||
|
) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => {
|
||||||
|
d.free_memory(memory, None);
|
||||||
|
d.destroy_buffer(buffer, None);
|
||||||
|
return Err(e).context("vkGetMemoryFdKHR(slot)");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let ext = match cuda::ExternalDmabuf::import_owned_fd(fd, reqs.size) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
d.free_memory(memory, None);
|
||||||
|
d.destroy_buffer(buffer, None);
|
||||||
|
return Err(e).context("cuImportExternalMemory(slot OPAQUE_FD)");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let r = VkSlotRef {
|
||||||
|
ptr: ext.ptr,
|
||||||
|
pitch: pitch as usize,
|
||||||
|
height,
|
||||||
|
id: self.slots.len(),
|
||||||
|
};
|
||||||
|
self.slots.push(SlotAlloc {
|
||||||
|
buffer,
|
||||||
|
memory,
|
||||||
|
cuda: ext,
|
||||||
|
size,
|
||||||
|
});
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Free every allocated slot (encoder teardown, alongside its ring clear). CUDA mappings drop
|
||||||
|
/// first (field order in [`SlotAlloc`] frees `cuda` via its own `Drop` before we free the VK
|
||||||
|
/// objects explicitly here).
|
||||||
|
pub fn free_slots(&mut self) {
|
||||||
|
for s in self.slots.drain(..) {
|
||||||
|
drop(s.cuda); // CUDA's view of the memory goes first
|
||||||
|
// SAFETY: `buffer`/`memory` were created in `alloc_slot`, are uniquely owned by the
|
||||||
|
// drained `SlotAlloc`, and are destroyed exactly once here. No queue work can be
|
||||||
|
// in flight: every `blend` fence-waits before returning.
|
||||||
|
unsafe {
|
||||||
|
self.device.destroy_buffer(s.buffer, None);
|
||||||
|
self.device.free_memory(s.memory, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload the cursor RGBA (`cw*ch*4`, tight rows) into the mapped staging buffer. Call only
|
||||||
|
/// when the bitmap changes; position moves are push constants.
|
||||||
|
pub fn upload_cursor(&mut self, rgba: &[u8], cw: u32, ch: u32) {
|
||||||
|
let cw = cw.min(CURSOR_MAX);
|
||||||
|
let ch = ch.min(CURSOR_MAX);
|
||||||
|
let len = (cw * ch * 4) as usize;
|
||||||
|
let len = len.min(rgba.len());
|
||||||
|
// SAFETY: `cur_map` is the live persistent mapping of the CURSOR_MAX²·4 host-coherent
|
||||||
|
// allocation (created in `init_objects`, unmapped only in `Drop`); `len` is clamped to
|
||||||
|
// both the source slice and the buffer capacity. No blend is in flight (every `blend`
|
||||||
|
// fence-waits before returning), so no GPU read races this host write.
|
||||||
|
unsafe {
|
||||||
|
std::ptr::copy_nonoverlapping(rgba.as_ptr(), self.cur_map, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blend the uploaded cursor into `slot` at `(ox, oy)`: record, submit, fence-wait. The
|
||||||
|
/// caller has CPU-synced its CUDA frame copy first; the fence wait makes the shader's writes
|
||||||
|
/// visible to the subsequent NVENC encode (the `VkBridge` precedent: fence-ordered cross-API
|
||||||
|
/// access, no queue-family transfer — NVIDIA-only path).
|
||||||
|
#[allow(clippy::too_many_arguments)] // surface geometry + cursor rect — unpacked kernel args
|
||||||
|
pub fn blend_ref(
|
||||||
|
&mut self,
|
||||||
|
slot: &VkSlotRef,
|
||||||
|
fmt: SlotFormat,
|
||||||
|
surf_w: u32,
|
||||||
|
cw: u32,
|
||||||
|
ch: u32,
|
||||||
|
ox: i32,
|
||||||
|
oy: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let alloc = self
|
||||||
|
.slots
|
||||||
|
.get(slot.id)
|
||||||
|
.ok_or_else(|| anyhow!("bad slot id {}", slot.id))?;
|
||||||
|
let cw = cw.min(CURSOR_MAX);
|
||||||
|
let ch = ch.min(CURSOR_MAX);
|
||||||
|
if cw == 0 || ch == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let push = Push {
|
||||||
|
pitch: slot.pitch as u32,
|
||||||
|
surf_w,
|
||||||
|
surf_h: slot.height,
|
||||||
|
cur_w: cw,
|
||||||
|
cur_h: ch,
|
||||||
|
ox,
|
||||||
|
oy,
|
||||||
|
};
|
||||||
|
// Dispatch geometry (must match cursor_blend.comp): ARGB = per cursor px; NV12/YUV444 =
|
||||||
|
// per word-aligned 4-px span × (2-row blocks | rows).
|
||||||
|
let (gx, gy) = match fmt {
|
||||||
|
SlotFormat::Argb => (cw.div_ceil(8), ch.div_ceil(8)),
|
||||||
|
_ => {
|
||||||
|
let x0 = (ox >> 2) << 2;
|
||||||
|
let spans = ((ox + cw as i32) - x0 + 3).div_euclid(4).max(1) as u32;
|
||||||
|
let rows = match fmt {
|
||||||
|
SlotFormat::Nv12 => ch.div_ceil(2),
|
||||||
|
_ => ch,
|
||||||
|
};
|
||||||
|
(spans.div_ceil(8), rows.div_ceil(8))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// SAFETY: single-threaded record/submit/wait on handles this struct owns. The descriptor
|
||||||
|
// update is safe because no prior submission is in flight (every blend fence-waits and
|
||||||
|
// the fence is reset before reuse). Buffer infos and barrier structs are locals outliving
|
||||||
|
// their synchronous calls. The dispatch's shader accesses stay in-bounds by the shader's
|
||||||
|
// own guards (surfW/surfH/curW/curH from `push`) against the slot allocation sized in
|
||||||
|
// `alloc_slot` for exactly that geometry.
|
||||||
|
unsafe {
|
||||||
|
let d = &self.device;
|
||||||
|
let surf_info = [vk::DescriptorBufferInfo::default()
|
||||||
|
.buffer(alloc.buffer)
|
||||||
|
.offset(0)
|
||||||
|
.range(alloc.size)];
|
||||||
|
let cur_info = [vk::DescriptorBufferInfo::default()
|
||||||
|
.buffer(self.cur_buf)
|
||||||
|
.offset(0)
|
||||||
|
.range((CURSOR_MAX * CURSOR_MAX * 4) as u64)];
|
||||||
|
let writes = [
|
||||||
|
vk::WriteDescriptorSet::default()
|
||||||
|
.dst_set(self.desc_set)
|
||||||
|
.dst_binding(0)
|
||||||
|
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
|
||||||
|
.buffer_info(&surf_info),
|
||||||
|
vk::WriteDescriptorSet::default()
|
||||||
|
.dst_set(self.desc_set)
|
||||||
|
.dst_binding(1)
|
||||||
|
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
|
||||||
|
.buffer_info(&cur_info),
|
||||||
|
];
|
||||||
|
d.update_descriptor_sets(&writes, &[]);
|
||||||
|
|
||||||
|
d.begin_command_buffer(
|
||||||
|
self.cmd,
|
||||||
|
&vk::CommandBufferBeginInfo::default()
|
||||||
|
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
|
||||||
|
)
|
||||||
|
.context("begin blend cmd")?;
|
||||||
|
// CUDA wrote the frame into this memory outside Vulkan's view — make it visible to
|
||||||
|
// the shader (external-memory coherence ceremony; NVIDIA honors this with the fence
|
||||||
|
// ordering alone, the barrier is the spec-shaped belt-and-braces).
|
||||||
|
let acquire = [vk::MemoryBarrier::default()
|
||||||
|
.src_access_mask(vk::AccessFlags::MEMORY_WRITE)
|
||||||
|
.dst_access_mask(vk::AccessFlags::SHADER_READ | vk::AccessFlags::SHADER_WRITE)];
|
||||||
|
d.cmd_pipeline_barrier(
|
||||||
|
self.cmd,
|
||||||
|
vk::PipelineStageFlags::TOP_OF_PIPE,
|
||||||
|
vk::PipelineStageFlags::COMPUTE_SHADER,
|
||||||
|
vk::DependencyFlags::empty(),
|
||||||
|
&acquire,
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
d.cmd_bind_pipeline(
|
||||||
|
self.cmd,
|
||||||
|
vk::PipelineBindPoint::COMPUTE,
|
||||||
|
self.pipelines[fmt.mode() as usize],
|
||||||
|
);
|
||||||
|
d.cmd_bind_descriptor_sets(
|
||||||
|
self.cmd,
|
||||||
|
vk::PipelineBindPoint::COMPUTE,
|
||||||
|
self.pipe_layout,
|
||||||
|
0,
|
||||||
|
&[self.desc_set],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
let bytes = std::slice::from_raw_parts(
|
||||||
|
(&push as *const Push) as *const u8,
|
||||||
|
std::mem::size_of::<Push>(),
|
||||||
|
);
|
||||||
|
d.cmd_push_constants(
|
||||||
|
self.cmd,
|
||||||
|
self.pipe_layout,
|
||||||
|
vk::ShaderStageFlags::COMPUTE,
|
||||||
|
0,
|
||||||
|
bytes,
|
||||||
|
);
|
||||||
|
d.cmd_dispatch(self.cmd, gx.max(1), gy.max(1), 1);
|
||||||
|
// Release the shader's writes so the post-fence CUDA/NVENC reads see them.
|
||||||
|
let release = [vk::MemoryBarrier::default()
|
||||||
|
.src_access_mask(vk::AccessFlags::SHADER_WRITE)
|
||||||
|
.dst_access_mask(vk::AccessFlags::MEMORY_READ)];
|
||||||
|
d.cmd_pipeline_barrier(
|
||||||
|
self.cmd,
|
||||||
|
vk::PipelineStageFlags::COMPUTE_SHADER,
|
||||||
|
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
|
||||||
|
vk::DependencyFlags::empty(),
|
||||||
|
&release,
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
d.end_command_buffer(self.cmd).context("end blend cmd")?;
|
||||||
|
let cmds = [self.cmd];
|
||||||
|
let submit = [vk::SubmitInfo::default().command_buffers(&cmds)];
|
||||||
|
d.queue_submit(self.queue, &submit, self.fence)
|
||||||
|
.context("submit blend")?;
|
||||||
|
let r = d.wait_for_fences(&[self.fence], true, 1_000_000_000);
|
||||||
|
d.reset_fences(&[self.fence]).ok();
|
||||||
|
r.context("blend fence wait")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for VkSlotBlend {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.free_slots();
|
||||||
|
// SAFETY: every handle below was created in `new`/`init_objects` (or is null from a
|
||||||
|
// partial init — Vulkan destroy/free calls are defined no-ops on null handles) and is
|
||||||
|
// uniquely owned; each is destroyed exactly once here, pipelines/layouts/pools before the
|
||||||
|
// device, the device before the instance. No work is in flight (`blend` fence-waits).
|
||||||
|
unsafe {
|
||||||
|
let d = &self.device;
|
||||||
|
for p in self.pipelines {
|
||||||
|
if p != vk::Pipeline::null() {
|
||||||
|
d.destroy_pipeline(p, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.shader != vk::ShaderModule::null() {
|
||||||
|
d.destroy_shader_module(self.shader, None);
|
||||||
|
}
|
||||||
|
if self.desc_pool != vk::DescriptorPool::null() {
|
||||||
|
d.destroy_descriptor_pool(self.desc_pool, None);
|
||||||
|
}
|
||||||
|
if self.pipe_layout != vk::PipelineLayout::null() {
|
||||||
|
d.destroy_pipeline_layout(self.pipe_layout, None);
|
||||||
|
}
|
||||||
|
if self.desc_layout != vk::DescriptorSetLayout::null() {
|
||||||
|
d.destroy_descriptor_set_layout(self.desc_layout, None);
|
||||||
|
}
|
||||||
|
if !self.cur_map.is_null() {
|
||||||
|
d.unmap_memory(self.cur_mem);
|
||||||
|
}
|
||||||
|
if self.cur_buf != vk::Buffer::null() {
|
||||||
|
d.destroy_buffer(self.cur_buf, None);
|
||||||
|
}
|
||||||
|
if self.cur_mem != vk::DeviceMemory::null() {
|
||||||
|
d.free_memory(self.cur_mem, None);
|
||||||
|
}
|
||||||
|
if self.fence != vk::Fence::null() {
|
||||||
|
d.destroy_fence(self.fence, None);
|
||||||
|
}
|
||||||
|
if self.cmd_pool != vk::CommandPool::null() {
|
||||||
|
d.destroy_command_pool(self.cmd_pool, None);
|
||||||
|
}
|
||||||
|
d.destroy_device(None);
|
||||||
|
self.instance.destroy_instance(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user