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}.
This commit is contained in:
2026-06-19 01:32:28 +03:00
parent 977c056287
commit bcf708d3cc
13 changed files with 371 additions and 28 deletions
+1
View File
@@ -11,6 +11,7 @@ add_executable(vgpu-streamer
src/stream/win32/region.c
src/stream/win32/present.c
src/stream/win32/cursor.c
src/stream/win32/geometry.c
src/stream/win32/capture.c
src/stream/win32/capture_nvfbc.c
src/stream/win32/capture_dda.c
+42
View File
@@ -26,6 +26,12 @@ enum { VGPU_FMT_BGRA8888 = 0 };
enum { VGPU_ST_INIT=0, VGPU_ST_CAPTURING=1, VGPU_ST_PAUSED=2, VGPU_ST_STOPPED=3, VGPU_ST_ERROR=4 };
enum { VGPU_BK_NONE=0, VGPU_BK_NVFBC=1, VGPU_BK_DDA=2, VGPU_BK_GDI=3 };
enum { VGPU_CMD_STOP=0, VGPU_CMD_RUN=1, VGPU_CMD_PAUSE=2 };
/* cursor shape identity (wire-uint32); UNKNOWN=0 → custom/unrecognized glyph */
enum { VGPU_CURSOR_ID_UNKNOWN=0, VGPU_CURSOR_ID_ARROW=1, VGPU_CURSOR_ID_IBEAM=2,
VGPU_CURSOR_ID_WAIT=3, VGPU_CURSOR_ID_CROSS=4, VGPU_CURSOR_ID_HAND=5,
VGPU_CURSOR_ID_SIZENS=6, VGPU_CURSOR_ID_SIZEWE=7, VGPU_CURSOR_ID_SIZENWSE=8,
VGPU_CURSOR_ID_SIZENESW=9, VGPU_CURSOR_ID_SIZEALL=10, VGPU_CURSOR_ID_NO=11,
VGPU_CURSOR_ID_APPSTARTING=12 };
/* ===== Per-slot descriptor (under hot.seq[slot]) ===== */
typedef struct {
@@ -81,6 +87,26 @@ typedef struct {
atomic MOV. low 32 bits = x, high 32 = y, each a
signed int32 (two's-complement; multi-monitor →
negatives). Pair never tears (one 64-bit store). */
/* --- cursor Tier-1 (host-RO; same cursor_seq gate as cursor_pos/visible) --- */
uint32_t cursor_hotspot; /* @184: low16=hot_x, high16=hot_y (unsigned) */
uint32_t cursor_glyph; /* @188: low16=glyph_w, high16=glyph_h (unsigned) */
uint32_t cursor_id; /* @192: VGPU_CURSOR_ID_* shape identity */
/* --- graphics static-idle: monotonic stamp of last scene-content change --- */
alignas(8) uint64_t content_change_ns; /* @200: host derives idle-ms vs its own clock */
/* --- display geometry (own cache line; geom_seq seqlock; sampled rarely) ---
* captured-surface SIZE is NOT here: it is desc.width/height (authoritative, tight). */
alignas(64)
uint32_t geom_seq; /* @256: even=stable, odd=writing (frame-seqlock) */
int32_t virt_x; /* @260: virtual-desktop origin (signed) */
int32_t virt_y; /* @264 */
uint32_t virt_w; /* @268: virtual-desktop bbox size (interprets neg pos) */
uint32_t virt_h; /* @272 */
int32_t cap_x; /* @276: captured-output origin in virtual-desktop coords */
int32_t cap_y; /* @280: (captured size = desc.width/height, not here) */
uint32_t dpi; /* @284: captured-output effective DPI; 96=100%; 0=unknown */
uint32_t refresh_mhz; /* @288: captured-output refresh in milli-Hz; 0=unknown */
} vgpu_producer_t;
static_assert(alignof(vgpu_producer_t) == 64, "producer align");
static_assert(sizeof(vgpu_producer_t) <= VGPU_PAGE, "producer fits page 0");
@@ -101,6 +127,22 @@ static_assert(offsetof(vgpu_producer_t, full_frame_ack) == 164, "producer.ful
static_assert(offsetof(vgpu_producer_t, cursor_seq) == 168, "producer.cursor_seq");
static_assert(offsetof(vgpu_producer_t, cursor_visible) == 172, "producer.cursor_visible");
static_assert(offsetof(vgpu_producer_t, cursor_pos) == 176, "producer.cursor_pos");
/* cursor Tier-1 (cursor line, gated by cursor_seq) */
static_assert(offsetof(vgpu_producer_t, cursor_hotspot) == 184, "producer.cursor_hotspot");
static_assert(offsetof(vgpu_producer_t, cursor_glyph) == 188, "producer.cursor_glyph");
static_assert(offsetof(vgpu_producer_t, cursor_id) == 192, "producer.cursor_id");
/* graphics static-idle */
static_assert(offsetof(vgpu_producer_t, content_change_ns) == 200, "producer.content_change_ns");
/* display geometry (own cache line; captured SIZE is desc.width/height, not here) */
static_assert(offsetof(vgpu_producer_t, geom_seq) == 256, "producer.geom_seq");
static_assert(offsetof(vgpu_producer_t, virt_x) == 260, "producer.virt_x");
static_assert(offsetof(vgpu_producer_t, virt_y) == 264, "producer.virt_y");
static_assert(offsetof(vgpu_producer_t, virt_w) == 268, "producer.virt_w");
static_assert(offsetof(vgpu_producer_t, virt_h) == 272, "producer.virt_h");
static_assert(offsetof(vgpu_producer_t, cap_x) == 276, "producer.cap_x");
static_assert(offsetof(vgpu_producer_t, cap_y) == 280, "producer.cap_y");
static_assert(offsetof(vgpu_producer_t, dpi) == 284, "producer.dpi");
static_assert(offsetof(vgpu_producer_t, refresh_mhz) == 288, "producer.refresh_mhz");
/* ===== Control block (host-RW), own page, generation-guarded ===== */
typedef struct {
+25
View File
@@ -60,4 +60,29 @@ void vgpu_publish_full_frame_ack(const vgpu_region_view* rv, uint32_t req);
* field (single atomic store) and bumps cursor_seq last. */
void vgpu_publish_cursor(const vgpu_region_view* rv, int32_t x, int32_t y, uint32_t visible);
/* Publish Tier-1 cursor shape data (host-RO), written under the same cursor_seq gate as
* vgpu_publish_cursor: call this BEFORE vgpu_publish_cursor so the position publish bumps
* cursor_seq last and gates the whole cursor line consistently. hot_x/hot_y are the glyph
* hotspot; gw/gh are glyph dims; cursor_id is a VGPU_CURSOR_ID_* shape identity. */
void vgpu_publish_cursor_shape(const vgpu_region_view* rv,
uint32_t hot_x, uint32_t hot_y,
uint32_t gw, uint32_t gh, uint32_t cursor_id);
/* Publish the monotonic timestamp (ns) of the last scene-content change. Single 8-aligned
* atomic store (heartbeat pattern). The producer reports the raw stamp only; the host derives
* "ms idle" by subtracting from its own clock — no behavioural distillation in the producer. */
void vgpu_publish_content_change(const vgpu_region_view* rv, uint64_t change_ns);
/* Publish display geometry under the geom_seq seqlock (odd/even, like the frame seqlock).
* Sampled rarely (session start + reactive resample on desc-size delta / backend recreate),
* read by the host with bounded retry. virt_* is the virtual-desktop bbox (interprets negative
* cursor_pos); cap_x/cap_y is the captured output's origin in virtual-desktop coords (the
* captured surface SIZE comes from desc.width/height, not from here). dpi/refresh_mhz describe
* the captured output (96=100% / milli-Hz; 0=unknown). */
void vgpu_publish_geometry(const vgpu_region_view* rv,
int32_t virt_x, int32_t virt_y,
uint32_t virt_w, uint32_t virt_h,
int32_t cap_x, int32_t cap_y,
uint32_t dpi, uint32_t refresh_mhz);
#endif /* VGPU_STREAM_ENGINE_H */
+34
View File
@@ -127,3 +127,37 @@ void vgpu_publish_cursor(const vgpu_region_view* rv, int32_t x, int32_t y, uint3
/* publish seq last: its release-store gates the pos/visible writes above for the host */
vgpu_store_release32(&p->cursor_seq, p->cursor_seq + 1u);
}
void vgpu_publish_cursor_shape(const vgpu_region_view* rv, uint32_t hot_x, uint32_t hot_y,
uint32_t gw, uint32_t gh, uint32_t cursor_id) {
vgpu_producer_t* p = rv->producer;
/* pack 16|16 strictly unsigned (mask low half so no sign bits bleed into the high half).
* No own seq: the following vgpu_publish_cursor bumps cursor_seq last and gates this line. */
vgpu_store_release32(&p->cursor_hotspot, (hot_y << 16) | (hot_x & 0xFFFFu));
vgpu_store_release32(&p->cursor_glyph, (gh << 16) | (gw & 0xFFFFu));
vgpu_store_release32(&p->cursor_id, cursor_id);
}
void vgpu_publish_content_change(const vgpu_region_view* rv, uint64_t change_ns) {
/* 64-bit aligned single MOV is atomic on x86_64; barrier orders it (heartbeat pattern) */
rv->producer->content_change_ns = change_ns;
vgpu_compiler_barrier();
}
void vgpu_publish_geometry(const vgpu_region_view* rv, int32_t virt_x, int32_t virt_y,
uint32_t virt_w, uint32_t virt_h,
int32_t cap_x, int32_t cap_y,
uint32_t dpi, uint32_t refresh_mhz) {
vgpu_producer_t* p = rv->producer;
/* seqlock: even -> odd (writing) */
vgpu_store_release32(&p->geom_seq, p->geom_seq + 1u);
vgpu_compiler_barrier();
p->virt_x = virt_x; p->virt_y = virt_y;
p->virt_w = virt_w; p->virt_h = virt_h;
p->cap_x = cap_x; p->cap_y = cap_y;
p->dpi = dpi; p->refresh_mhz = refresh_mhz;
vgpu_sfence();
/* seqlock: odd -> even (stable) */
vgpu_store_release32(&p->geom_seq, p->geom_seq + 1u);
vgpu_sfence();
}
+71 -2
View File
@@ -9,6 +9,9 @@
#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;
@@ -17,8 +20,61 @@ typedef struct {
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;
@@ -33,12 +89,18 @@ static DWORD WINAPI dda_thread(LPVOID param) {
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)))
(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) {
@@ -76,7 +138,14 @@ int dda_start(vgpu_ctx* ctx, int fps) {
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) output->lpVtbl->QueryInterface(output, &IID_IDXGIOutput1, (void**)&st->out1);
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);
+18
View File
@@ -6,6 +6,9 @@
#include "capture_gdi.h"
#include "capture-win32.h" /* capture_thread_arg (win32-private) */
#include "present.h"
#include "cursor.h" /* cursor_sample (position+shape+id) for compose+publish */
#include "geometry.h" /* reactive geometry resample on capture-size change */
#include "stream.h" /* vgpu_publish_cursor / vgpu_publish_cursor_shape */
static DWORD WINAPI gdi_thread(LPVOID param) {
capture_thread_arg* arg = (capture_thread_arg*)param;
@@ -38,10 +41,25 @@ static DWORD WINAPI gdi_thread(LPVOID param) {
SelectObject(mem, dib);
W = w; H = h;
fprintf(stderr, "eyes(gdi): desktop %dx%d (BitBlt; cursor by presenter)\n", W, H);
/* capture size changed (primary at origin (0,0)) → resample geometry */
geometry_sample_and_publish(ctx, 0, 0);
}
if (BitBlt(mem, 0, 0, W, H, screen, 0, 0, SRCCOPY))
vgpu_present_submit(ctx, (const uint8_t*)bits,
(uint32_t)W, (uint32_t)H, (uint32_t)W * 4u);
/* source the cursor for present's compositing (under ctx->lock) and publish it */
EnterCriticalSection(&ctx->lock);
cursor_sample(ctx);
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;
int32_t cx = (int32_t)ctx->cursor.x, cy = (int32_t)ctx->cursor.y;
uint32_t cvis = (uint32_t)(ctx->cursor.visible != 0);
LeaveCriticalSection(&ctx->lock);
vgpu_publish_cursor_shape(&ctx->view, hx, hy, gw, gh, cid);
vgpu_publish_cursor(&ctx->view, cx, cy, cvis);
Sleep(interval);
}
return 0; /* unreachable; satisfies -Wreturn-type */
+35 -1
View File
@@ -6,14 +6,45 @@
#include "capture_nvfbc.h"
#include "capture-win32.h" /* capture_thread_arg (win32-private) */
#include "present.h"
#include "cursor.h" /* cursor_apply_shape / ctx->cursor */
#include "geometry.h" /* reactive geometry resample on recreate */
#include "stream.h" /* vgpu_publish_cursor / vgpu_publish_cursor_shape */
#include "nvfbc_tosys_c.h"
typedef struct {
NvFBCToSys_c* fbc;
void* buf;
NvFBC_CreateFunctionExType create;
HCURSOR last_handle; /* shape-gate by HCURSOR change */
} nvfbc_state;
/* Source the cursor for an NvFBC grab and publish it under the cursor_seq gate. NvFBC reports
* only HW-cursor visibility (gi.bHWMouseVisible); position is not exposed, so one GetCursorInfo
* per frame supplies x/y (the minimum possible). Shape is re-extracted only on HCURSOR change.
* NvFBC composites the cursor itself (draw_cursor_cap==0) → present never reads ctx->cursor for
* drawing, so no ctx->lock is required around the compose fields here.
* gi.bProtectedContent / gi.dwSourcePID are available but out of scope (not in the contract). */
static void nvfbc_source_cursor(vgpu_ctx* ctx, nvfbc_state* st,
const NvFBCFrameGrabInfo* gi) {
CURSORINFO ci; ci.cbSize = sizeof ci;
int vis = gi->bHWMouseVisible ? 1 : 0;
int x = ctx->cursor.x, y = ctx->cursor.y;
if (GetCursorInfo(&ci)) {
x = ci.ptScreenPos.x; y = ci.ptScreenPos.y;
if (ci.hCursor && ci.hCursor != st->last_handle) {
cursor_apply_shape(ctx, ci.hCursor);
st->last_handle = ci.hCursor;
}
}
ctx->cursor.visible = vis; ctx->cursor.x = x; ctx->cursor.y = y;
vgpu_publish_cursor_shape(&ctx->view,
(uint32_t)ctx->cursor.hot_x, (uint32_t)ctx->cursor.hot_y,
(uint32_t)ctx->cursor.gw, (uint32_t)ctx->cursor.gh,
(uint32_t)ctx->cursor.cursor_id);
vgpu_publish_cursor(&ctx->view, (int32_t)x, (int32_t)y, (uint32_t)vis);
}
static NvFBCToSys_c* nvfbc_create(NvFBC_CreateFunctionExType pCreate, void** ppBuf) {
NvFBCCreateParams cp; memset(&cp, 0, sizeof cp);
cp.dwVersion = NVFBC_CREATE_PARAMS_VER;
@@ -62,6 +93,8 @@ static DWORD WINAPI nvfbc_thread(LPVOID param) {
fbc = NULL;
while (!(fbc = nvfbc_create(st->create, &buf))) Sleep(200);
st->fbc = fbc; st->buf = buf;
/* grab session was recreated → display config may have changed: resample */
geometry_sample_and_publish(ctx, 0, 0);
} else {
Sleep(50);
}
@@ -70,6 +103,7 @@ static DWORD WINAPI nvfbc_thread(LPVOID param) {
if (gi.dwWidth && gi.dwHeight)
vgpu_present_submit(ctx, (const uint8_t*)buf,
gi.dwWidth, gi.dwHeight, gi.dwBufferWidth * 4u);
nvfbc_source_cursor(ctx, st, &gi);
}
return 0; /* unreachable; satisfies -Wreturn-type */
}
@@ -109,7 +143,7 @@ int nvfbc_start(vgpu_ctx* ctx, int fps) {
nvfbc_state* st = (nvfbc_state*)malloc(sizeof *st);
if (!st) { fbc->lpVtbl->NvFBCToSysRelease(fbc); return 0; }
st->fbc = fbc; st->buf = buf; st->create = pCreate;
st->fbc = fbc; st->buf = buf; st->create = pCreate; st->last_handle = NULL;
capture_thread_arg* arg = (capture_thread_arg*)malloc(sizeof *arg);
if (!arg) { fbc->lpVtbl->NvFBCToSysRelease(fbc); free(st); return 0; }
+1
View File
@@ -30,6 +30,7 @@ typedef struct {
int x, y;
int hot_x, hot_y;
int gw, gh; /* glyph dims */
int cursor_id; /* VGPU_CURSOR_ID_* resolved on shape change */
int mono; /* 1 = AND/XOR monochrome cursor */
uint8_t* bgra; /* color cursor BGRA (arena) */
uint8_t* and_mask; /* mono AND (arena) */
+38 -9
View File
@@ -2,6 +2,7 @@
#include <windows.h>
#include <string.h>
#include "cursor.h"
#include "vgpu_stream.h" /* VGPU_CURSOR_ID_* */
/* Max supported cursor glyph; buffers are pre-arena'd in ctx (no heap here). */
#define VGPU_CURSOR_MAX 256
@@ -85,6 +86,42 @@ static void extract(vgpu_ctx* ctx, HCURSOR hc) {
if (ii.hbmMask) DeleteObject(ii.hbmMask);
}
int cursor_resolve_id(HCURSOR hc) {
/* System-cursor table loaded once (IDC_* are stable per session). Lazy: built on first
* call, then a linear handle compare. UNKNOWN for custom/unrecognized cursors. */
static const struct { LPCTSTR idc; int id; } kSpec[] = {
{ IDC_ARROW, VGPU_CURSOR_ID_ARROW },
{ IDC_IBEAM, VGPU_CURSOR_ID_IBEAM },
{ IDC_WAIT, VGPU_CURSOR_ID_WAIT },
{ IDC_CROSS, VGPU_CURSOR_ID_CROSS },
{ IDC_HAND, VGPU_CURSOR_ID_HAND },
{ IDC_SIZENS, VGPU_CURSOR_ID_SIZENS },
{ IDC_SIZEWE, VGPU_CURSOR_ID_SIZEWE },
{ IDC_SIZENWSE, VGPU_CURSOR_ID_SIZENWSE },
{ IDC_SIZENESW, VGPU_CURSOR_ID_SIZENESW },
{ IDC_SIZEALL, VGPU_CURSOR_ID_SIZEALL },
{ IDC_NO, VGPU_CURSOR_ID_NO },
{ IDC_APPSTARTING, VGPU_CURSOR_ID_APPSTARTING },
};
enum { N = (int)(sizeof kSpec / sizeof kSpec[0]) };
static HCURSOR cache[N];
static int loaded = 0;
if (!loaded) {
for (int i = 0; i < N; i++) cache[i] = LoadCursor(NULL, kSpec[i].idc);
loaded = 1;
}
if (!hc) return VGPU_CURSOR_ID_UNKNOWN;
for (int i = 0; i < N; i++)
if (cache[i] == hc) return kSpec[i].id;
return VGPU_CURSOR_ID_UNKNOWN;
}
void cursor_apply_shape(vgpu_ctx* ctx, HCURSOR hc) {
extract(ctx, hc);
ctx->cursor.cursor_id = cursor_resolve_id(hc);
ctx->cursor.handle = hc;
}
int cursor_sample(vgpu_ctx* ctx) {
vgpu_cursor_t* cur = &ctx->cursor;
CURSORINFO ci; ci.cbSize = sizeof ci;
@@ -99,21 +136,13 @@ int cursor_sample(vgpu_ctx* ctx) {
|| (ci.hCursor != cur->handle);
if (vis && ci.hCursor && ci.hCursor != cur->handle) {
extract(ctx, ci.hCursor);
cur->cursor_id = cursor_resolve_id(ci.hCursor);
cur->handle = ci.hCursor;
}
cur->visible = vis; cur->x = x; cur->y = y;
return changed;
}
void cursor_sample_pos(vgpu_ctx* ctx) {
vgpu_cursor_t* cur = &ctx->cursor;
CURSORINFO ci; ci.cbSize = sizeof ci;
if (!GetCursorInfo(&ci)) { cur->visible = 0; return; }
cur->visible = (ci.flags & CURSOR_SHOWING) != 0;
cur->x = ci.ptScreenPos.x;
cur->y = ci.ptScreenPos.y;
}
void cursor_draw(vgpu_ctx* ctx, uint8_t* dst, uint32_t W, uint32_t H) {
vgpu_cursor_t* cur = &ctx->cursor;
if (!cur->visible || cur->gw == 0) return;
+9 -3
View File
@@ -10,9 +10,15 @@
* Returns 1 if anything changed since last sample, else 0. */
int cursor_sample(vgpu_ctx* ctx);
/* Sample only cursor position/visibility (no shape extract). Updates
* ctx->cursor.{visible,x,y}. Cheap (one GetCursorInfo); safe to call every pump tick. */
void cursor_sample_pos(vgpu_ctx* ctx);
/* Resolve a HCURSOR to a VGPU_CURSOR_ID_* by comparing against the system cursor table
* (LoadCursor(NULL, IDC_*) loaded once on first use). Returns VGPU_CURSOR_ID_UNKNOWN for
* custom cursors. Not hot-path: called only under the shape-change gate. */
int cursor_resolve_id(HCURSOR hc);
/* Extract glyph/hotspot/dims for hc into ctx->cursor, resolve its cursor_id, and record it as
* the current handle. For backends that source position elsewhere (DDA from frame info) and
* only need the shape on a shape-change gate. Caller serializes ctx->cursor writes. */
void cursor_apply_shape(vgpu_ctx* ctx, HCURSOR hc);
/* Alpha/AND-XOR compose the sampled cursor onto a tight BGRA frame. */
void cursor_draw(vgpu_ctx* ctx, uint8_t* bgra, uint32_t width, uint32_t height);
+52
View File
@@ -0,0 +1,52 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "geometry.h"
#include "stream.h" /* vgpu_publish_geometry */
/* GetDpiForMonitor lives in Shcore.dll (per-monitor DPI awareness API). Loaded dynamically so
* the binary does not hard-depend on it; absence degrades dpi to "unknown" (0). */
typedef HRESULT (WINAPI *GetDpiForMonitor_t)(HMONITOR, int /*MDT_*/, UINT*, UINT*);
#define VGPU_MDT_EFFECTIVE_DPI 0
static UINT monitor_dpi(HMONITOR mon) {
static GetDpiForMonitor_t fn = NULL;
static int tried = 0;
if (!tried) {
HMODULE lib = LoadLibraryA("Shcore.dll");
if (lib) fn = (GetDpiForMonitor_t)(void*)GetProcAddress(lib, "GetDpiForMonitor");
tried = 1;
}
if (!fn || !mon) return 0u;
UINT dx = 0, dy = 0;
if (fn(mon, VGPU_MDT_EFFECTIVE_DPI, &dx, &dy) != S_OK || dx == 0u)
return 0u;
return dx;
}
static uint32_t monitor_refresh_mhz(HMONITOR mon) {
MONITORINFOEXW mi; mi.cbSize = sizeof mi;
if (!mon || !GetMonitorInfoW(mon, (MONITORINFO*)&mi))
return 0u;
DEVMODEW dm; ZeroMemory(&dm, sizeof dm); dm.dmSize = sizeof dm;
if (!EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm))
return 0u;
if (dm.dmDisplayFrequency <= 1u) /* 0/1 = hardware default, not a real rate */
return 0u;
return (uint32_t)dm.dmDisplayFrequency * 1000u; /* whole Hz -> milli-Hz */
}
void geometry_sample_and_publish(vgpu_ctx* ctx, int32_t cap_x, int32_t cap_y) {
int32_t virt_x = (int32_t)GetSystemMetrics(SM_XVIRTUALSCREEN);
int32_t virt_y = (int32_t)GetSystemMetrics(SM_YVIRTUALSCREEN);
uint32_t virt_w = (uint32_t)GetSystemMetrics(SM_CXVIRTUALSCREEN);
uint32_t virt_h = (uint32_t)GetSystemMetrics(SM_CYVIRTUALSCREEN);
POINT origin = { cap_x, cap_y };
HMONITOR mon = MonitorFromPoint(origin, MONITOR_DEFAULTTOPRIMARY);
uint32_t dpi = monitor_dpi(mon);
uint32_t refresh = monitor_refresh_mhz(mon);
vgpu_publish_geometry(&ctx->view, virt_x, virt_y, virt_w, virt_h,
cap_x, cap_y, dpi, refresh);
}
+18
View File
@@ -0,0 +1,18 @@
#ifndef VGPU_GEOMETRY_H
#define VGPU_GEOMETRY_H
/* geometry.h — win32 display-geometry sampler. Samples the virtual-desktop bbox plus the
* captured output's origin / DPI / refresh and publishes them under the geom_seq seqlock.
* Not per-frame: called once at session start and reactively on backend recreate / capture-
* size change (the captured surface SIZE itself travels in desc.width/height, not here). */
#include <stdint.h>
#include "ctx.h" /* win32 vgpu_ctx (region-view) */
/* Sample display geometry for the captured output whose top-left origin is (cap_x,cap_y) in
* virtual-desktop coordinates, and publish it. cap_x/cap_y is (0,0) for primary/full-screen
* backends and the duplicated output's DesktopCoordinates for DDA. The captured size is taken
* from desc.width/height and is not sampled here. */
void geometry_sample_and_publish(vgpu_ctx* ctx, int32_t cap_x, int32_t cap_y);
#endif /* VGPU_GEOMETRY_H */
+27 -13
View File
@@ -5,6 +5,7 @@
#include "present.h"
#include "stream.h" /* OS-agnostic publish / control API + region-view */
#include "cursor.h"
#include "geometry.h" /* one-shot display-geometry sample at session start */
/* cursor arena sizing */
#define VGPU_CUR_MAX 256u
@@ -78,6 +79,9 @@ void vgpu_present_submit(vgpu_ctx* ctx, const uint8_t* src,
ctx->content_h = H;
ctx->content_seq++;
LeaveCriticalSection(&ctx->lock);
/* static-idle: stamp the moment the source delivered new content (the raw perception;
* the host derives "ms idle" from its own clock). Single 8-aligned MOV, off the lock. */
vgpu_publish_content_change(&ctx->view, now_ns());
SetEvent(ctx->submit_event);
}
@@ -89,6 +93,13 @@ void vgpu_present_run(vgpu_ctx* ctx) {
uint32_t last_ff_ack = rv->producer->full_frame_ack;
DWORD last_beat = GetTickCount();
uint64_t last_publish_ns = 0; /* 0 → first eligible frame publishes immediately */
int last_cur_x = 0, last_cur_y = 0, last_cur_vis = 0;
HCURSOR last_cur_handle = NULL;
/* one-shot display geometry: publish once before the loop (flat pull contract). The
* captured-output origin is (0,0) for the primary/full-screen capture path; backends
* resample reactively on recreate / capture-size change. No periodic poll in the loop. */
geometry_sample_and_publish(ctx, 0, 0);
for (;;) {
WaitForSingleObject(ctx->submit_event, poll_ms);
@@ -100,12 +111,6 @@ void vgpu_present_run(vgpu_ctx* ctx) {
last_beat = nowt;
}
/* --- cursor position: sensor data, reported every tick independent of
* draw_cursor / compositing (host may overlay it itself) --- */
cursor_sample_pos(ctx);
vgpu_publish_cursor(rv, (int32_t)ctx->cursor.x, (int32_t)ctx->cursor.y,
(uint32_t)(ctx->cursor.visible != 0));
/* --- reconcile control (gen-seqlock -> apply -> ack) --- */
vgpu_control_view cv;
uint32_t desired = prev_state;
@@ -152,18 +157,25 @@ void vgpu_present_run(vgpu_ctx* ctx) {
/* --- compose + publish on content change OR forced full frame, but
* rate-limited to the applied fps cap (the single publish point →
* contract-level cap, independent of the capture backend). A
* force_full bypasses the cap (due=1). --- */
int cur_changed = (ctx->draw_cursor_cap && draw_cursor)
? cursor_sample(ctx) : 0;
* force_full bypasses the cap (due=1). present does NOT sample the
* cursor (capture threads source it); it only reads ctx->cursor under
* ctx->lock for compositing, and detects cursor motion via a delta so
* a pure cursor move over static desktop still recomposes. --- */
uint64_t interval_ns = fps > 0 ? (1000000000ull / fps) : 0;
uint64_t now = now_ns();
int due = force_full || interval_ns == 0
|| (now - last_publish_ns) >= interval_ns;
int compose_cursor = (ctx->draw_cursor_cap && draw_cursor);
EnterCriticalSection(&ctx->lock);
int64_t seq = ctx->content_seq;
uint32_t W = ctx->content_w, H = ctx->content_h;
int cur_changed = compose_cursor
&& ((ctx->cursor.visible != last_cur_vis)
|| (ctx->cursor.x != last_cur_x)
|| (ctx->cursor.y != last_cur_y)
|| (ctx->cursor.handle != last_cur_handle));
int have = (W && H);
int content_new = have && (seq != last_seq || cur_changed || force_full);
/* take the frame ONLY when due — so we never drop the latest content;
@@ -172,6 +184,11 @@ void vgpu_present_run(vgpu_ctx* ctx) {
if (dirty) {
memcpy(ctx->frame_buf, ctx->content_buf, (size_t)W * H * 4u);
last_seq = seq;
if (compose_cursor)
cursor_draw(ctx, ctx->frame_buf, W, H);
last_cur_vis = ctx->cursor.visible;
last_cur_x = ctx->cursor.x; last_cur_y = ctx->cursor.y;
last_cur_handle = ctx->cursor.handle;
}
LeaveCriticalSection(&ctx->lock);
@@ -182,9 +199,6 @@ void vgpu_present_run(vgpu_ctx* ctx) {
continue;
}
if (ctx->draw_cursor_cap && draw_cursor)
cursor_draw(ctx, ctx->frame_buf, W, H);
if (vgpu_publish_frame(rv, ctx->frame_buf, W, H, now) == 0) {
last_publish_ns = now;
if (force_full) {