Files
vatrog-vm-vgpu-streamer/src/stream/win32/capture_dda.c
T
lirent bcf708d3cc feat: add graphics-context sensors to producer contract
Append three host-readable sensor groups to the producer block, ABI
frozen by offset asserts (producer still fits page 0):

- cursor Tier-1 (hotspot, glyph dims, shape identity), published under
  the existing cursor_seq gate
- content_change_ns: monotonic stamp of the last scene-content change
- display geometry on its own cache line under a geom_seq seqlock:
  virtual-desktop bbox, captured-output origin, DPI, refresh

Source the cursor from each backend's existing grab metadata instead of
polling GetCursorInfo in the present pump: DDA from the duplication frame
info pointer position (no extra calls), NvFBC visibility from the grab
info plus one GetCursorInfo per frame for position, GDI from
GetCursorInfo. Position sampling now rides the frame rate, not the pump
tick.

Sample display geometry once at startup and re-sample only on backend
session recreate or a captured-mode change. Drop per-tick cursor
sampling (cursor_sample_pos) from the present loop.

Add src/stream/win32/geometry.{c,h}.
2026-06-19 01:32:28 +03:00

199 lines
7.9 KiB
C

#define WIN32_LEAN_AND_MEAN
#define COBJMACROS
#include <windows.h>
#include <d3d11.h>
#include <dxgi1_2.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "capture_dda.h"
#include "capture-win32.h" /* capture_thread_arg (win32-private) */
#include "present.h"
#include "cursor.h" /* cursor_resolve_id + ctx->cursor compose state */
#include "geometry.h" /* reactive geometry resample on recreate */
#include "stream.h" /* vgpu_publish_cursor / vgpu_publish_cursor_shape */
typedef struct {
ID3D11Device* dev;
ID3D11DeviceContext* dctx;
IDXGIOutput1* out1;
IDXGIOutputDuplication* dup;
ID3D11Texture2D* staging;
UINT W, H;
int32_t cap_x, cap_y; /* captured output origin (virt coords) */
UINT64 last_mouse_update; /* shape-gate by fi.LastMouseUpdateTime */
int seeded; /* cold-start position seed done */
} dda_state;
/* Source the cursor from the already-fetched frame info (0 syscalls for position) and publish
* it under the cursor_seq gate. Position/visibility come from fi.PointerPosition; the shape is
* re-extracted only when fi.LastMouseUpdateTime changed (shape-gate). Cold start: fi is invalid
* until the mouse first moves (LastMouseUpdateTime==0) — seed the position once via one
* GetCursorInfo, then rely on fi. ctx->cursor compose fields are written under ctx->lock; the
* producer-block publish uses release/seq, no lock. */
static void dda_source_cursor(vgpu_ctx* ctx, dda_state* st,
const DXGI_OUTDUPL_FRAME_INFO* fi) {
int vis = fi->PointerPosition.Visible ? 1 : 0;
int x, y;
UINT64 upd = (UINT64)fi->LastMouseUpdateTime.QuadPart;
if (!st->seeded && upd == 0) {
CURSORINFO ci; ci.cbSize = sizeof ci;
if (GetCursorInfo(&ci)) {
vis = (ci.flags & CURSOR_SHOWING) != 0;
x = ci.ptScreenPos.x; y = ci.ptScreenPos.y;
} else {
x = ctx->cursor.x; y = ctx->cursor.y;
}
st->seeded = 1;
} else {
x = fi->PointerPosition.Position.x;
y = fi->PointerPosition.Position.y;
if (upd != 0) st->seeded = 1;
}
/* shape-gate: re-extract only when the mouse-update stamp advanced */
if (upd != 0 && upd != st->last_mouse_update) {
CURSORINFO ci; ci.cbSize = sizeof ci;
if (GetCursorInfo(&ci) && ci.hCursor && ci.hCursor != ctx->cursor.handle) {
EnterCriticalSection(&ctx->lock);
cursor_apply_shape(ctx, ci.hCursor);
LeaveCriticalSection(&ctx->lock);
}
st->last_mouse_update = upd;
}
EnterCriticalSection(&ctx->lock);
ctx->cursor.visible = vis;
ctx->cursor.x = x; ctx->cursor.y = y;
uint32_t hx = (uint32_t)ctx->cursor.hot_x, hy = (uint32_t)ctx->cursor.hot_y;
uint32_t gw = (uint32_t)ctx->cursor.gw, gh = (uint32_t)ctx->cursor.gh;
uint32_t cid = (uint32_t)ctx->cursor.cursor_id;
LeaveCriticalSection(&ctx->lock);
vgpu_publish_cursor_shape(&ctx->view, hx, hy, gw, gh, cid);
vgpu_publish_cursor(&ctx->view, (int32_t)x, (int32_t)y, (uint32_t)vis);
}
static DWORD WINAPI dda_thread(LPVOID param) {
capture_thread_arg* arg = (capture_thread_arg*)param;
vgpu_ctx* ctx = arg->ctx;
dda_state* st = (dda_state*)arg->backend_state;
free(arg);
for (;;) {
DXGI_OUTDUPL_FRAME_INFO fi;
IDXGIResource* res = NULL;
HRESULT hr = st->dup->lpVtbl->AcquireNextFrame(st->dup, 1000, &fi, &res);
if (hr == DXGI_ERROR_WAIT_TIMEOUT) continue;
if (hr == DXGI_ERROR_ACCESS_LOST) {
if (st->dup) { st->dup->lpVtbl->Release(st->dup); st->dup = NULL; }
if (FAILED(st->out1->lpVtbl->DuplicateOutput(st->out1,
(IUnknown*)st->dev, &st->dup))) {
Sleep(200);
} else {
/* display config may have changed across the access loss → resample geometry */
geometry_sample_and_publish(ctx, st->cap_x, st->cap_y);
}
continue;
}
if (FAILED(hr)) { Sleep(50); continue; }
dda_source_cursor(ctx, st, &fi);
ID3D11Texture2D* tex = NULL;
res->lpVtbl->QueryInterface(res, &IID_ID3D11Texture2D, (void**)&tex);
if (tex) {
st->dctx->lpVtbl->CopyResource(st->dctx,
(ID3D11Resource*)st->staging, (ID3D11Resource*)tex);
D3D11_MAPPED_SUBRESOURCE m;
if (SUCCEEDED(st->dctx->lpVtbl->Map(st->dctx,
(ID3D11Resource*)st->staging, 0, D3D11_MAP_READ, 0, &m))) {
vgpu_present_submit(ctx, (const uint8_t*)m.pData, st->W, st->H, m.RowPitch);
st->dctx->lpVtbl->Unmap(st->dctx, (ID3D11Resource*)st->staging, 0);
}
tex->lpVtbl->Release(tex);
}
if (res) res->lpVtbl->Release(res);
st->dup->lpVtbl->ReleaseFrame(st->dup);
}
return 0; /* unreachable; satisfies -Wreturn-type */
}
int dda_start(vgpu_ctx* ctx, int fps) {
(void)fps;
dda_state* st = (dda_state*)calloc(1, sizeof *st);
if (!st) return 0;
D3D_FEATURE_LEVEL fl;
if (FAILED(D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, NULL, 0,
D3D11_SDK_VERSION, &st->dev, &fl, &st->dctx))) {
fprintf(stderr, "eyes(dda): D3D11CreateDevice failed\n");
goto fail;
}
IDXGIDevice* dxgiDev = NULL;
IDXGIAdapter* adapter = NULL;
IDXGIOutput* output = NULL;
st->dev->lpVtbl->QueryInterface(st->dev, &IID_IDXGIDevice, (void**)&dxgiDev);
if (dxgiDev) dxgiDev->lpVtbl->GetAdapter(dxgiDev, &adapter);
if (adapter) adapter->lpVtbl->EnumOutputs(adapter, 0, &output);
if (output) {
DXGI_OUTPUT_DESC od;
if (SUCCEEDED(output->lpVtbl->GetDesc(output, &od))) {
st->cap_x = (int32_t)od.DesktopCoordinates.left;
st->cap_y = (int32_t)od.DesktopCoordinates.top;
}
output->lpVtbl->QueryInterface(output, &IID_IDXGIOutput1, (void**)&st->out1);
}
if (output) output->lpVtbl->Release(output);
if (adapter) adapter->lpVtbl->Release(adapter);
if (dxgiDev) dxgiDev->lpVtbl->Release(dxgiDev);
if (!st->out1 || FAILED(st->out1->lpVtbl->DuplicateOutput(st->out1,
(IUnknown*)st->dev, &st->dup))) {
fprintf(stderr, "eyes(dda): DuplicateOutput failed\n");
goto fail;
}
DXGI_OUTDUPL_DESC dd;
st->dup->lpVtbl->GetDesc(st->dup, &dd);
st->W = dd.ModeDesc.Width;
st->H = dd.ModeDesc.Height;
D3D11_TEXTURE2D_DESC td; memset(&td, 0, sizeof td);
td.Width = st->W; td.Height = st->H; td.MipLevels = 1; td.ArraySize = 1;
td.Format = DXGI_FORMAT_B8G8R8A8_UNORM; td.SampleDesc.Count = 1;
td.Usage = D3D11_USAGE_STAGING; td.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
if (FAILED(st->dev->lpVtbl->CreateTexture2D(st->dev, &td, NULL, &st->staging))) {
fprintf(stderr, "eyes(dda): CreateTexture2D failed\n");
goto fail;
}
capture_thread_arg* arg = (capture_thread_arg*)malloc(sizeof *arg);
if (!arg) goto fail;
arg->ctx = ctx; arg->fps = fps; arg->backend_state = st;
ctx->backend = VGPU_BK_DDA;
ctx->draw_cursor_cap = 1; /* DDA frames are content-only → presenter draws cursor */
HANDLE t = CreateThread(NULL, 0, dda_thread, arg, 0, NULL);
if (!t) { free(arg); goto fail; }
CloseHandle(t);
fprintf(stderr, "eyes(dda): desktop %ux%u (content-only; cursor by presenter)\n",
st->W, st->H);
return 1;
fail:
/* release any COM objects created before the failure (no ref leaks) */
if (st->staging) st->staging->lpVtbl->Release(st->staging);
if (st->dup) st->dup->lpVtbl->Release(st->dup);
if (st->out1) st->out1->lpVtbl->Release(st->out1);
if (st->dctx) st->dctx->lpVtbl->Release(st->dctx);
if (st->dev) st->dev->lpVtbl->Release(st->dev);
free(st);
return 0;
}