mcap/modules/Screen_Record/cursor.jai
Musa Mahmood 06a2aacc9c [0.1.3] Video capture phase 1: modules/Screen_Record and record-probe, Desktop Duplication to NVENC
- modules/Screen_Record: a region of one screen to H.264 in a fragmented MP4. A capture thread
  duplicates the screen (DuplicateOutput1, else DuplicateOutput) on a D3D11 device of its own on
  the screen's adapter, copies the region on the GPU, reads it back through three staging
  textures and draws the cursor in (colour, masked colour and monochrome shapes). An encoder
  thread feeds FFmpeg: h264_nvenc, else h264_amf, h264_qsv, h264_mf, libx264, each tried in turn;
  BGRA straight to NVENC, swscale to NV12 or YUV 4:2:0 (BT.709) for the rest. A frame is sent
  when the screen or cursor changed and its turn on an fps schedule is due, with a repeat each
  second of an unchanged screen; the encoder falling 8 frames behind drops the oldest. A lost
  duplication (mode change, secure desktop) is made again and recording goes on. record_start
  says fall_back when duplication cannot run (unsupported, too many, rotated screen). Regions
  under 240 x 180 are refused, then sizes are rounded down to even; a process whose view of the
  desktop is scaled is refused.
- tools/record_probe.jai (jai first.jai - record_probe): lists the screens, records a screen or
  region and reports source, encoder, frames captured, sent, written and dropped, time per stage
  and file size; -legacy, -cuda, -exclude-test, -animate, -snapshot DIR, -check FILE.
- On the RTX 5080 laptop: 60 s of 4K at 30 fps, 1802 frames, none dropped, 18 MB a minute; every
  change kept at 60 fps; plain DuplicateOutput works when DPI aware; a CUDA context changes
  nothing; WDA_EXCLUDEFROMCAPTURE is respected; QSV, Media Foundation and libx264 keep up too;
  a recording killed mid-way still plays. The two-GPU failure was not reproduced: the screens are
  on the NVIDIA GPU in this mode. The plan records the results.
2026-10-10 06:48:43 +01:00

49 lines
2.2 KiB
Plaintext

// The mouse cursor, drawn into frames read back from Desktop Duplication, which leaves it out of the
// image and hands over its shape and position separately. Three kinds of shape:
//
// COLOR BGRA, blended by its alpha.
// MASKED_COLOR BGRA, where alpha 0 replaces the pixel under it and 0xFF XORs it.
// MONOCHROME 1 bit a pixel, twice as tall: an AND mask over an XOR mask. AND 1 XOR 1 inverts the
// pixel under it, which is how the text I-beam shows on any background.
Cursor_Shape :: struct {
type: DXGI_OUTDUPL_POINTER_SHAPE_TYPE;
width, height, pitch: s32; // height as handed over: twice the cursor's for MONOCHROME
data: [..] u8;
}
// Draws shape with its top left at x, y (in the frame's pixels; it may reach past the edges) into
// BGRA pixels, rows packed.
cursor_draw :: (pixels: *u8, width: s32, height: s32, shape: *Cursor_Shape, x: s32, y: s32) {
if !shape.data.count return;
rows := ifx shape.type == .MONOCHROME then shape.height / 2 else shape.height;
for row: 0..rows - 1 {
dy := y + row;
if dy < 0 || dy >= height continue;
for column: 0..shape.width - 1 {
dx := x + column;
if dx < 0 || dx >= width continue;
d := pixels + (cast(s64) dy * width + dx) * 4;
if shape.type == {
case .COLOR;
s := shape.data.data + row * shape.pitch + column * 4;
a := cast(u32) s[3];
if a == 0 continue;
for c: 0..2 d[c] = cast(u8) ((cast(u32) s[c] * a + cast(u32) d[c] * (255 - a) + 127) / 255);
case .MASKED_COLOR;
s := shape.data.data + row * shape.pitch + column * 4;
if s[3] == 0 for c: 0..2 d[c] = s[c];
else for c: 0..2 d[c] ^= s[c];
case .MONOCHROME;
bit := cast(u8) (0x80 >> (column % 8));
and_mask := shape.data[row * shape.pitch + column / 8] & bit;
xor_mask := shape.data[(row + rows) * shape.pitch + column / 8] & bit;
if and_mask && !xor_mask continue; // transparent
if and_mask { for c: 0..2 d[c] = 255 - d[c]; continue; } // inverted
value: u8 = ifx xor_mask then cast(u8) 255 else 0; // white or black
for c: 0..2 d[c] = value;
}
}
}
}