diff --git a/CMakeLists.txt b/CMakeLists.txt index 54fa2ac..ff7d5cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/include/vgpu_stream.h b/include/vgpu_stream.h index 1f1adc6..87cbe06 100644 --- a/include/vgpu_stream.h +++ b/include/vgpu_stream.h @@ -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 { diff --git a/src/stream/include/stream.h b/src/stream/include/stream.h index 5044aaa..9ea7e29 100644 --- a/src/stream/include/stream.h +++ b/src/stream/include/stream.h @@ -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 */ diff --git a/src/stream/publish.c b/src/stream/publish.c index cfb3d7f..d1cf2d1 100644 --- a/src/stream/publish.c +++ b/src/stream/publish.c @@ -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(); +} diff --git a/src/stream/win32/capture_dda.c b/src/stream/win32/capture_dda.c index 3bb9979..74e6d24 100644 --- a/src/stream/win32/capture_dda.c +++ b/src/stream/win32/capture_dda.c @@ -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); diff --git a/src/stream/win32/capture_gdi.c b/src/stream/win32/capture_gdi.c index 012b7fc..71df921 100644 --- a/src/stream/win32/capture_gdi.c +++ b/src/stream/win32/capture_gdi.c @@ -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 */ diff --git a/src/stream/win32/capture_nvfbc.c b/src/stream/win32/capture_nvfbc.c index 163df7e..0086c22 100644 --- a/src/stream/win32/capture_nvfbc.c +++ b/src/stream/win32/capture_nvfbc.c @@ -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; } diff --git a/src/stream/win32/ctx.h b/src/stream/win32/ctx.h index caf1b23..977604c 100644 --- a/src/stream/win32/ctx.h +++ b/src/stream/win32/ctx.h @@ -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) */ diff --git a/src/stream/win32/cursor.c b/src/stream/win32/cursor.c index 2ce60ea..5941cb1 100644 --- a/src/stream/win32/cursor.c +++ b/src/stream/win32/cursor.c @@ -2,6 +2,7 @@ #include #include #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; diff --git a/src/stream/win32/cursor.h b/src/stream/win32/cursor.h index efe52ee..81d4842 100644 --- a/src/stream/win32/cursor.h +++ b/src/stream/win32/cursor.h @@ -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); diff --git a/src/stream/win32/geometry.c b/src/stream/win32/geometry.c new file mode 100644 index 0000000..39bfa2d --- /dev/null +++ b/src/stream/win32/geometry.c @@ -0,0 +1,52 @@ +#define WIN32_LEAN_AND_MEAN +#include +#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); +} diff --git a/src/stream/win32/geometry.h b/src/stream/win32/geometry.h new file mode 100644 index 0000000..02d339f --- /dev/null +++ b/src/stream/win32/geometry.h @@ -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 +#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 */ diff --git a/src/stream/win32/present.c b/src/stream/win32/present.c index f31f3fd..c9513ec 100644 --- a/src/stream/win32/present.c +++ b/src/stream/win32/present.c @@ -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) {