1#define WIN32_LEAN_AND_MEAN
2#define UNICODE
3#define _UNICODE
4#include <windows.h>
5#include "cursor.h"
6#include "capture.h"
7
8typedef HCURSOR (WINAPI *cursor_frame_fn)(HCURSOR, DWORD, DWORD, DWORD *, DWORD *);
9static cursor_frame_fn cursor_frame;
10static HANDLE output;
11static HANDLE heap;
12static OVERLAPPED sending;
13static int modern_windows;
14
15static uint32_t microseconds(LONGLONG ticks, LONGLONG frequency) {
16 while (frequency > 0x7fffffff) { frequency >>= 1; ticks >>= 1; }
17 if (ticks > 0x7fffffff) return 0;
18 int value = MulDiv((int)ticks, 1000000, (int)frequency);
19 return value < 0 ? 0 : (uint32_t)value;
20}
21
22static int write_bytes(const void *data, DWORD length) {
23 const uint8_t *bytes = data;
24 DWORD written;
25 while (length) {
26 ResetEvent(sending.hEvent);
27 DWORD amount = length > 65536 ? 65536 : length;
28 if (!WriteFile(output, bytes, amount, &written, &sending)) {
29 if (GetLastError() != ERROR_IO_PENDING) return 0;
30 if (WaitForSingleObject(sending.hEvent, 3000) != WAIT_OBJECT_0) {
31 CancelIo(output); WaitForSingleObject(sending.hEvent, INFINITE); return 0;
32 }
33 }
34 if (!GetOverlappedResult(output, &sending, &written, FALSE)) return 0;
35 if (!written) return 0;
36 bytes += written; length -= written;
37 }
38 return 1;
39}
40
41static int packet(uint32_t type, uint32_t length, const uint32_t *words, uint32_t count) {
42 uint8_t header[12 + (SG_WINDOWS_WORDS + SG_MAX_WINDOWS * SG_WINDOW_WORDS) * 4];
43 uint32_t i;
44 if (count > SG_WINDOWS_WORDS + SG_MAX_WINDOWS * SG_WINDOW_WORDS) return 0;
45 header[0] = SG_MAGIC[0]; header[1] = SG_MAGIC[1]; header[2] = SG_MAGIC[2]; header[3] = SG_MAGIC[3];
46 sg_word(header + 4, type); sg_word(header + 8, length);
47 for (i = 0; i < count; ++i) sg_word(header + 12 + i * 4, words[i]);
48 return write_bytes(header, 12 + count * 4);
49}
50
51static int emit(uint32_t type, const uint32_t *words, uint32_t count, const uint8_t *pixels, uint32_t stride) {
52 uint32_t w = 0, h = 0;
53 if (pixels) {
54 w = words[type == SG_SCREEN ? SG_SCREEN_RECT_WIDTH : SG_SURFACE_RECT_WIDTH];
55 h = words[type == SG_SCREEN ? SG_SCREEN_RECT_HEIGHT : SG_SURFACE_RECT_HEIGHT];
56 }
57 if (!packet(type, count * 4 + w * h * 4, words, count)) return 0;
58 if (!pixels) return 1;
59 if (stride == w * 4) return write_bytes(pixels, w * h * 4);
60 uint8_t *packed = HeapAlloc(heap, 0, (size_t)w * h * 4);
61 if (!packed) return 0;
62 for (uint32_t y = 0; y < h; ++y) {
63 const uint8_t *src = pixels + (size_t)y * stride;
64 uint8_t *dst = packed + (size_t)y * w * 4;
65 for (uint32_t x = 0; x < w * 4; ++x) dst[x] = src[x];
66 }
67 int success = write_bytes(packed, w * h * 4);
68 HeapFree(heap, 0, packed);
69 return success;
70}
71
72static int capture_cursor(HCURSOR cursor, uint32_t id) {
73 ICONINFO icon = {0};
74 BITMAP bitmap;
75 BITMAPINFO info = {0};
76 HDC dc = NULL;
77 HBITMAP dib = NULL, previous = NULL;
78 uint8_t *bits = NULL, *black = NULL, *frames = NULL;
79 uint32_t width, height, count = 1, step, words[SG_CURSOR_WORDS] = {0};
80 DWORD rate = 6, total = 1;
81 size_t pixels, frame_bytes;
82 int success = -1;
83 if (!GetIconInfo(cursor, &icon)) return -1;
84 if (!GetObjectW(icon.hbmColor ? icon.hbmColor : icon.hbmMask, sizeof(bitmap), &bitmap)) goto done;
85 width = (uint32_t)bitmap.bmWidth;
86 height = (uint32_t)(icon.hbmColor ? bitmap.bmHeight : bitmap.bmHeight / 2);
87 if (!width || !height || width > SG_MAX_CURSOR_SIZE || height > SG_MAX_CURSOR_SIZE || icon.xHotspot >= width || icon.yHotspot >= height) goto done;
88 if (cursor_frame && cursor_frame(cursor, 0, 0, &rate, &total) && total > 1 && total <= SG_MAX_CURSOR_FRAMES) count = total;
89 pixels = (size_t)width * height;
90 frame_bytes = 4 + pixels * 5;
91 frames = HeapAlloc(heap, 0, frame_bytes * count);
92 black = HeapAlloc(heap, 0, pixels * 4);
93 dc = CreateCompatibleDC(NULL);
94 info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); info.bmiHeader.biWidth = (LONG)width;
95 info.bmiHeader.biHeight = -(LONG)height; info.bmiHeader.biPlanes = 1; info.bmiHeader.biBitCount = 32;
96 dib = CreateDIBSection(dc, &info, DIB_RGB_COLORS, (void **)&bits, NULL, 0);
97 if (!frames || !black || !dc || !dib) goto done;
98 previous = SelectObject(dc, dib);
99 for (step = 0; step < count; ++step) {
100 uint8_t *frame = frames + frame_bytes * step;
101 size_t i;
102 HCURSOR image = cursor;
103 rate = 6;
104 if (count > 1) {
105 image = cursor_frame(cursor, 0, step, &rate, &total);
106 if (!image || total != count || !rate || rate > 3600) { image = cursor; count = 1; rate = 6; }
107 }
108 sg_word(frame, (rate * 1000u + 30u) / 60u);
109 for (i = 0; i < pixels * 4; ++i) bits[i] = 0;
110 if (!DrawIconEx(dc, 0, 0, image, (int)width, (int)height, 0, NULL, DI_NORMAL)) goto done;
111 GdiFlush();
112 for (i = 0; i < pixels * 4; ++i) black[i] = bits[i];
113 for (i = 0; i < pixels * 4; ++i) bits[i] = 255;
114 if (!DrawIconEx(dc, 0, 0, image, (int)width, (int)height, 0, NULL, DI_NORMAL)) goto done;
115 GdiFlush();
116 if (!sg_rgba(black, bits, frame + 4, frame + 4 + pixels * 4, pixels)) goto done;
117 }
118 words[SG_CURSOR_ID] = id; words[SG_CURSOR_WIDTH] = width; words[SG_CURSOR_HEIGHT] = height;
119 words[SG_CURSOR_HOT_X] = icon.xHotspot; words[SG_CURSOR_HOT_Y] = icon.yHotspot; words[SG_CURSOR_FRAMES] = count;
120 success = packet(SG_CURSOR, SG_CURSOR_WORDS * 4 + (uint32_t)(frame_bytes * count), words, SG_CURSOR_WORDS) && write_bytes(frames, (DWORD)(frame_bytes * count));
121done:
122 if (previous) SelectObject(dc, previous);
123 if (dib) DeleteObject(dib);
124 if (dc) DeleteDC(dc);
125 if (icon.hbmColor) DeleteObject(icon.hbmColor);
126 if (icon.hbmMask) DeleteObject(icon.hbmMask);
127 if (black) HeapFree(heap, 0, black);
128 if (frames) HeapFree(heap, 0, frames);
129 return success;
130}
131
132static void stream(DWORD session) {
133 HDC screen = GetDC(NULL), dc = CreateCompatibleDC(screen);
134 HBITMAP dib = NULL, previous = NULL;
135 uint8_t *pixels = NULL, *before = NULL;
136 uint32_t width = 0, height = 0, id = 0;
137 HCURSOR last_cursor = NULL;
138 POINT last_point = {-1, -1};
139 DWORD last_flags = ~0u, cursor_retry = 0;
140 DWORD heartbeat = GetTickCount(), received = 0;
141 LARGE_INTEGER frequency, now, next_capture, started, scanned, written;
142 uint32_t stats[SG_CAPTURE_STATS_WORDS] = {0};
143 HMODULE winmm = LoadLibraryW(L"winmm.dll");
144 typedef UINT (WINAPI *period_fn)(UINT);
145 period_fn begin_period = winmm ? (period_fn)GetProcAddress(winmm, "timeBeginPeriod") : NULL;
146 period_fn end_period = winmm ? (period_fn)GetProcAddress(winmm, "timeEndPeriod") : NULL;
147 OVERLAPPED receiving = {0};
148 uint8_t request[16];
149 DWORD request_size = 12;
150 int pending = 0;
151 int suspended = 0, initial = 1, capturing = 1;
152 int windows_enabled = 0, windows_active = 0, cursor_supported = 1;
153 QueryPerformanceFrequency(&frequency); QueryPerformanceCounter(&next_capture);
154 if (begin_period && end_period) begin_period(1);
155 receiving.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
156 if (!screen || !dc || !receiving.hEvent) goto done;
157 SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED);
158 if (frequency.QuadPart <= 0 || frequency.QuadPart > 0xffffffffu) goto done;
159 uint32_t capabilities[SG_CAPABILITIES_WORDS] = {modern_windows, 1};
160 if (!emit(SG_CAPABILITIES, capabilities, SG_CAPABILITIES_WORDS, NULL, 0)) goto done;
161 for (;;) {
162 if (session != WTSGetActiveConsoleSessionId()) goto done;
163 CURSORINFO cursor = {sizeof(CURSORINFO), 0, NULL, {0, 0}};
164 HDESK desktop;
165 WCHAR name[32];
166 DWORD length;
167 DWORD amount = 0;
168 if (!pending) {
169 ResetEvent(receiving.hEvent);
170 if (!ReadFile(output, request + received, request_size - received, &amount, &receiving)) {
171 if (GetLastError() != ERROR_IO_PENDING) goto done;
172 pending = 1;
173 } else if (!GetOverlappedResult(output, &receiving, &amount, FALSE)) goto done;
174 } else if (GetOverlappedResult(output, &receiving, &amount, FALSE)) pending = 0;
175 else if (GetLastError() != ERROR_IO_INCOMPLETE) goto done;
176 if (!pending) {
177 if (!amount) goto done;
178 received += amount;
179 if (received == 12 && request_size == 12) {
180 uint32_t i;
181 for (i = 0; i < 4; ++i) if (request[i] != SG_MAGIC[i]) goto done;
182 if (request[4] || request[5] || request[6] || request[8] || request[9] || request[10]) goto done;
183 if (request[7] == SG_REFRESH && !request[11]) {
184 received = 0; initial = 1; last_cursor = NULL; last_flags = ~0u;
185 if (!emit(SG_CAPABILITIES, capabilities, SG_CAPABILITIES_WORDS, NULL, 0)) goto done;
186 } else if (request[7] == SG_STOP && !request[11]) {
187 packet(SG_STOP, 0, NULL, 0);
188 goto done;
189 } else if ((request[7] == SG_EXPERIMENT || request[7] == SG_STREAM) && request[11] == 4) request_size = 16;
190 else goto done;
191 }
192 if (received == 16) {
193 if (request[12] || request[13] || request[14] || request[15] > 1) goto done;
194 if (request[7] == SG_STREAM) {
195 if (capturing != request[15]) {
196 capturing = request[15];
197 SetThreadExecutionState(ES_CONTINUOUS | (capturing ? ES_DISPLAY_REQUIRED : 0));
198 initial = 1; last_cursor = NULL; last_flags = ~0u;
199 if (!capturing) { sg_capture_stop(); windows_active = 0; }
200 }
201 } else {
202 int enabled = request[15] && modern_windows;
203 if (enabled != windows_enabled) { sg_windows_stop(); windows_active = 0; }
204 windows_enabled = enabled;
205 if (!windows_enabled) {
206 uint32_t disabled[SG_WINDOWS_WORDS] = {0};
207 if (!emit(SG_WINDOWS, disabled, SG_WINDOWS_WORDS, NULL, 0)) goto done;
208 }
209 }
210 received = 0; request_size = 12;
211 }
212 }
213 if ((DWORD)(GetTickCount() - heartbeat) >= 1000) {
214 stats[SG_CAPTURE_STATS_ELAPSED_MS] = GetTickCount() - heartbeat;
215 if (!emit(SG_CAPTURE_STATS, stats, SG_CAPTURE_STATS_WORDS, NULL, 0)) goto done;
216 for (uint32_t i = 0; i < SG_CAPTURE_STATS_WORDS; ++i) stats[i] = 0;
217 if (!packet(SG_HEARTBEAT, 0, NULL, 0)) goto done;
218 heartbeat = GetTickCount();
219 }
220 if (!capturing) { Sleep(10); continue; }
221 desktop = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS);
222 int active = desktop && GetUserObjectInformationW(desktop, UOI_NAME, name, sizeof(name), &length) && name[0] == L'D' && name[1] == L'e' && name[2] == L'f' && name[3] == L'a' && name[4] == L'u' && name[5] == L'l' && name[6] == L't' && !name[7];
223 if (desktop) CloseDesktop(desktop);
224 if (!active || !GetCursorInfo(&cursor)) {
225 if (!suspended && !packet(SG_SUSPEND, 0, NULL, 0)) goto done;
226 suspended = 1; initial = 1; last_cursor = NULL; last_flags = ~0u;
227 sg_capture_stop();
228 windows_active = 0;
229 Sleep(100); continue;
230 }
231 int changed = cursor.hCursor && cursor.hCursor != last_cursor;
232 if (suspended) {
233 if (!emit(SG_CAPABILITIES, capabilities, SG_CAPABILITIES_WORDS, NULL, 0)) goto done;
234 suspended = 0;
235 }
236 if (changed) ++id;
237 if (changed || cursor.flags != last_flags || cursor.ptScreenPos.x != last_point.x || cursor.ptScreenPos.y != last_point.y) {
238 uint32_t words[SG_POINTER_WORDS] = {0};
239 words[SG_POINTER_ID] = id; words[SG_POINTER_X] = (uint32_t)cursor.ptScreenPos.x;
240 words[SG_POINTER_Y] = (uint32_t)cursor.ptScreenPos.y; words[SG_POINTER_VISIBLE] = (cursor.flags & CURSOR_SHOWING) ? 1 : 0;
241 if (!packet(SG_POINTER, sizeof(words), words, SG_POINTER_WORDS)) goto done;
242 last_flags = cursor.flags; last_point = cursor.ptScreenPos;
243 }
244 if (changed || (!cursor_supported && (DWORD)(GetTickCount() - cursor_retry) >= 1000)) {
245 int result = capture_cursor(cursor.hCursor, id);
246 if (!result) goto done;
247 if (result < 0) {
248 uint32_t words[SG_CURSOR_WORDS] = {0};
249 words[SG_CURSOR_ID] = id;
250 if (!packet(SG_CURSOR, sizeof(words), words, SG_CURSOR_WORDS)) goto done;
251 }
252 last_cursor = cursor.hCursor;
253 cursor_supported = result > 0; cursor_retry = GetTickCount();
254 }
255 QueryPerformanceCounter(&now);
256 if (now.QuadPart >= next_capture.QuadPart) {
257 next_capture.QuadPart = now.QuadPart + (uint32_t)frequency.QuadPart / 60;
258 started = now;
259 uint32_t w = (uint32_t)GetSystemMetrics(SM_CXSCREEN), h = (uint32_t)GetSystemMetrics(SM_CYSCREEN), y;
260 struct sg_rect rect = {0};
261 uint32_t words[SG_SCREEN_WORDS] = {0};
262 if (!w || !h || w > SG_MAX_DIMENSION || h > SG_MAX_DIMENSION || w * h > SG_MAX_PIXELS) goto done;
263 if (w != width || h != height) {
264 BITMAPINFO info = {0};
265 if (previous) { SelectObject(dc, previous); previous = NULL; }
266 if (dib) DeleteObject(dib);
267 if (before) HeapFree(heap, 0, before);
268 before = HeapAlloc(heap, 0, (size_t)w * h * 4);
269 info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); info.bmiHeader.biWidth = (LONG)w;
270 info.bmiHeader.biHeight = -(LONG)h; info.bmiHeader.biPlanes = 1; info.bmiHeader.biBitCount = 32;
271 dib = CreateDIBSection(dc, &info, DIB_RGB_COLORS, (void **)&pixels, NULL, 0);
272 if (!before || !dib) goto done;
273 previous = SelectObject(dc, dib); width = w; height = h; initial = 1;
274 }
275 int captured = modern_windows ? sg_desktop_capture(pixels, width, height, initial) : -1;
276 if (!captured) {
277 if (initial) continue;
278 goto window_capture;
279 }
280 if (captured < 0 && !BitBlt(dc, 0, 0, (int)width, (int)height, screen, 0, 0, SRCCOPY | CAPTUREBLT)) {
281 if (!suspended && !packet(SG_SUSPEND, 0, NULL, 0)) goto done;
282 suspended = 1; initial = 1; Sleep(100); continue;
283 }
284 if (captured < 0) GdiFlush();
285 QueryPerformanceCounter(&now);
286 ++stats[SG_CAPTURE_STATS_FRAMES];
287 stats[SG_CAPTURE_STATS_CAPTURE_US] += microseconds(now.QuadPart - started.QuadPart, frequency.QuadPart);
288 scanned = now;
289 if (initial) { rect.x = 0; rect.y = 0; rect.width = width; rect.height = height; }
290 else if (!sg_difference(before, pixels, width, height, &rect)) rect.width = rect.height = 0;
291 QueryPerformanceCounter(&written);
292 stats[SG_CAPTURE_STATS_SCAN_US] += microseconds(written.QuadPart - scanned.QuadPart, frequency.QuadPart);
293 words[SG_SCREEN_WIDTH] = width; words[SG_SCREEN_HEIGHT] = height;
294 words[SG_SCREEN_X] = rect.x; words[SG_SCREEN_Y] = rect.y;
295 words[SG_SCREEN_RECT_WIDTH] = rect.width; words[SG_SCREEN_RECT_HEIGHT] = rect.height;
296 if (rect.width && !emit(SG_SCREEN, words, SG_SCREEN_WORDS, pixels + ((size_t)rect.y * width + rect.x) * 4, width * 4)) goto done;
297 for (y = rect.y; y < rect.y + rect.height; ++y) {
298 size_t offset = ((size_t)y * width + rect.x) * 4, i;
299 for (i = 0; i < rect.width * 4; ++i) before[offset + i] = pixels[offset + i];
300 }
301window_capture:
302 if (windows_enabled) {
303 if (!windows_active) {
304 windows_active = sg_capture_start();
305 if (!windows_active) {
306 uint32_t disabled[SG_WINDOWS_WORDS] = {0};
307 if (!emit(SG_WINDOWS, disabled, SG_WINDOWS_WORDS, NULL, 0)) goto done;
308 windows_enabled = 0;
309 }
310 }
311 if (windows_active && !sg_windows_poll(emit, initial, pixels, width, height)) goto done;
312 }
313 QueryPerformanceCounter(&now);
314 if (captured) stats[SG_CAPTURE_STATS_WRITE_US] += microseconds(now.QuadPart - written.QuadPart, frequency.QuadPart);
315 initial = 0; suspended = 0;
316 }
317 Sleep(1);
318 }
319done:
320 SetThreadExecutionState(ES_CONTINUOUS);
321 sg_capture_stop();
322 if (begin_period && end_period) end_period(1);
323 if (winmm) FreeLibrary(winmm);
324 CancelIo(output);
325 if (pending) WaitForSingleObject(receiving.hEvent, INFINITE);
326 if (receiving.hEvent) CloseHandle(receiving.hEvent);
327 if (previous) SelectObject(dc, previous);
328 if (dib) DeleteObject(dib);
329 if (before) HeapFree(heap, 0, before);
330 if (dc) DeleteDC(dc);
331 if (screen) ReleaseDC(NULL, screen);
332}
333
334void mainCRTStartup(void) {
335 FARPROC symbol;
336 DWORD session;
337 if (!ProcessIdToSessionId(GetCurrentProcessId(), &session)) ExitProcess(1);
338 heap = GetProcessHeap();
339 sending.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
340 if (!sending.hEvent) ExitProcess(1);
341 SetProcessDPIAware();
342 typedef LONG (WINAPI *version_fn)(OSVERSIONINFOW *);
343 version_fn version = (version_fn)GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlGetVersion");
344 OSVERSIONINFOW os = {0}; os.dwOSVersionInfoSize = sizeof(os);
345 modern_windows = version && !version(&os) && os.dwMajorVersion >= 10 && os.dwBuildNumber >= 19041;
346 symbol = GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetCursorFrameInfo");
347 /* This optional user32 export supplies ANI step timing; static capture uses only documented APIs. */
348 cursor_frame = (cursor_frame_fn)symbol;
349 for (;;) {
350 sg_capture_stop();
351 if (session != WTSGetActiveConsoleSessionId()) { Sleep(1000); continue; }
352 output = CreateFileW(L"\\\\.\\Global\\" SG_CHANNEL, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
353 if (output != INVALID_HANDLE_VALUE) { stream(session); CloseHandle(output); }
354 Sleep(1000);
355 }
356}