验证:补充独立光标可行性POC

This commit is contained in:
宋一鸣 2026-11-03 17:49:31 +08:00
parent b29b59c62f
commit 1439028833
49 changed files with 957 additions and 1 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View File

@ -0,0 +1 @@
{"width": 1, "height": 1, "hotX": 0, "hotY": 0, "x": 320, "y": 1250, "argb": [0]}

View File

@ -0,0 +1 @@
{"width": 32, "height": 32, "hotX": 0, "hotY": 0, "x": 320, "y": 500, "argb": [4278255885, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294967295, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294507003, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4293980403, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4293454058, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4292993507, 4278387726, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294243575, 4294967295, 4294967295, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294243575, 4278255630, 4289967292, 4294967295, 4278387726, 4278782222, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294243575, 4278255630, 4278848013, 4278321678, 4294967295, 4292993507, 4278321678, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294243575, 4278255630, 4278848013, 0, 4278453264, 4290953929, 4294967295, 4278453264, 4278782222, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278387726, 4278255630, 4278848013, 0, 0, 4278848013, 4278321678, 4294967295, 4292269784, 4278321678, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278453264, 4292269784, 4294967295, 4278387214, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278979085, 4278321678, 4278519056, 4278913294, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

View File

@ -0,0 +1,5 @@
{
"clicks": 32,
"correct_regions_and_shapes": 32,
"max_coordinate_quantization_px": 1
}

View File

@ -0,0 +1,3 @@
{"action":"Query","device":"\\\\.\\DISPLAY3","hz":60,"width":2560,"bpp":32,"height":1440}
task_result=0

View File

@ -0,0 +1,10 @@
{"Name":"NVIDIA GeForce GTX 965M","ConfigManagerErrorCode":0}
NVIDIA GeForce GTX 965M, 582.78, 11 %
cursor_probe_task_present=False
[2026-11-03 14:29:49.548]: Info: Found H.264 encoder: h264_nvenc [nvenc]
[2026-11-03 14:29:50.970]: Info: Creating encoder [h264_nvenc]
[2026-11-03 14:34:22.851]: Info: Creating encoder [h264_nvenc]

View File

@ -0,0 +1,2 @@
{'width': 1, 'height': 1, 'hotX': 0, 'hotY': 0, 'x': 100, 'y': 100}
visible_local_pixels 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

View File

@ -0,0 +1 @@
{"width": 1, "height": 1, "hotX": 0, "hotY": 0, "x": 320, "y": 1250, "argb": [0]}

View File

@ -0,0 +1 @@
{"width": 32, "height": 32, "hotX": 0, "hotY": 0, "x": 320, "y": 500, "argb": [4278255885, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294967295, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294507003, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4293980403, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4293454058, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4292993507, 4278387726, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294967295, 4294243575, 4294967295, 4294967295, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294967295, 4294243575, 4278255630, 4289967292, 4294967295, 4278387726, 4278782222, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294967295, 4294243575, 4278255630, 4278848013, 4278321678, 4294967295, 4292993507, 4278321678, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278255885, 4294243575, 4278255630, 4278848013, 0, 4278453264, 4290953929, 4294967295, 4278453264, 4278782222, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278387726, 4278255630, 4278848013, 0, 0, 4278848013, 4278321678, 4294967295, 4292269784, 4278321678, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278453264, 4292269784, 4294967295, 4278387214, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278979085, 4278321678, 4278519056, 4278913294, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}

View File

@ -0,0 +1 @@
{"width": 32, "height": 32, "hotX": 8, "hotY": 9, "x": 960, "y": 500, "argb": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 4294112499, 4294112499, 0, 4294112499, 4294112499, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 4294112499, 4294112499, 0, 4294112499, 4294112499, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}

View File

@ -0,0 +1 @@
{"version": 1, "shape": "arrow", "visible": true, "expires": 1793689477.871138}

View File

@ -0,0 +1,44 @@
[
{
"shape": "arrow",
"x": 320,
"y": 500,
"visible": true
},
{
"shape": "ibeam",
"x": 960,
"y": 500,
"visible": true
},
{
"shape": "sizewe",
"x": 1600,
"y": 500,
"visible": true
},
{
"shape": "wait",
"x": 2240,
"y": 500,
"visible": true
},
{
"shape": "__hidden",
"x": 320,
"y": 1250,
"visible": false
},
{
"test": "client-paused-local-motion",
"before": [
320,
500
],
"after": [
500,
650
],
"passed": true
}
]

View File

@ -0,0 +1 @@
{"width": 32, "height": 32, "hotX": 11, "hotY": 4, "x": 1600, "y": 500, "argb": [0, 0, 0, 4278848013, 4278387726, 4278387726, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278387726, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278848013, 4278387726, 4294967295, 4278190093, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278190093, 4293980403, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278848013, 4278387726, 4294967295, 4294967295, 4278190093, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278190093, 4294967295, 4293717230, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278848013, 4278387726, 4294967295, 4294967295, 4294967295, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4294967295, 4294967295, 4293454058, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278387726, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4293454058, 4278387726, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278848013, 4278387726, 4294967295, 4294967295, 4294967295, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4294967295, 4294967295, 4293454058, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278848013, 4278387726, 4294967295, 4294967295, 4278190093, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278190093, 4294967295, 4293454058, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278848013, 4278387726, 4294967295, 4278190093, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278190093, 4293454058, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278848013, 4278387726, 4278387726, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278387726, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,74 @@
{"version":1,"shape":"wait","visible":true,"x":2240,"y":741,"utc":"2026-11-03T07:12:30.9503413Z"}
utc,x,y,shape
2026-11-03T07:11:23.6728439Z,320,600,arrow
2026-11-03T07:11:23.9853459Z,959,600,ibeam
2026-11-03T07:11:24.2703418Z,1600,600,sizewe
2026-11-03T07:11:24.5743423Z,2240,600,wait
2026-11-03T07:11:24.8668434Z,320,620,arrow
2026-11-03T07:11:25.1643408Z,959,620,ibeam
2026-11-03T07:11:25.4458407Z,1600,620,sizewe
2026-11-03T07:11:25.7348467Z,2240,620,wait
2026-11-03T07:11:26.0408467Z,320,639,arrow
2026-11-03T07:11:26.3293390Z,959,639,ibeam
2026-11-03T07:11:26.6163416Z,1600,639,sizewe
2026-11-03T07:11:26.9133420Z,2240,639,wait
2026-11-03T07:11:27.1828416Z,320,659,arrow
2026-11-03T07:11:27.4833409Z,959,659,ibeam
2026-11-03T07:11:27.7798422Z,1600,659,sizewe
2026-11-03T07:11:28.0623467Z,2240,659,wait
2026-11-03T07:11:28.3813431Z,320,679,arrow
2026-11-03T07:11:28.6843441Z,959,679,ibeam
2026-11-03T07:11:28.9513522Z,1600,679,sizewe
2026-11-03T07:11:29.2338429Z,2240,679,wait
2026-11-03T07:11:29.5318423Z,320,699,arrow
2026-11-03T07:11:29.8163521Z,959,699,ibeam
2026-11-03T07:11:30.1223411Z,1600,699,sizewe
2026-11-03T07:11:30.4248457Z,2240,699,wait
2026-11-03T07:11:30.6963403Z,320,720,arrow
2026-11-03T07:11:30.9818405Z,959,720,ibeam
2026-11-03T07:11:31.2808460Z,1600,720,sizewe
2026-11-03T07:11:31.5703482Z,2240,720,wait
2026-11-03T07:11:31.8683454Z,320,740,arrow
2026-11-03T07:11:32.1633457Z,959,740,ibeam
2026-11-03T07:11:32.4473379Z,1600,740,sizewe
2026-11-03T07:11:32.7883589Z,2240,740,wait
utc,shape,flags,x,y
2026-11-03T07:11:06.8498407Z,wait,1,2559,0
2026-11-03T07:11:16.6898849Z,arrow,1,320,499
2026-11-03T07:11:19.9603457Z,ibeam,1,959,499
2026-11-03T07:11:20.6268581Z,sizewe,1,1600,499
2026-11-03T07:11:21.2778534Z,wait,1,2240,499
2026-11-03T07:11:21.9288721Z,unknown,0,320,1250
2026-11-03T07:11:22.4868707Z,arrow,1,320,499
2026-11-03T07:11:23.8973466Z,ibeam,1,959,600
2026-11-03T07:11:24.2383472Z,sizewe,1,1600,600
2026-11-03T07:11:24.5638838Z,wait,1,2240,600
2026-11-03T07:11:24.8898404Z,arrow,1,320,620
2026-11-03T07:11:25.1063443Z,ibeam,1,959,620
2026-11-03T07:11:25.4318717Z,sizewe,1,1600,620
2026-11-03T07:11:25.7573475Z,wait,1,2240,620
2026-11-03T07:11:25.9898858Z,arrow,1,320,639
2026-11-03T07:11:26.3153463Z,ibeam,1,959,639
2026-11-03T07:11:26.6408719Z,sizewe,1,1600,639
2026-11-03T07:11:26.8578756Z,wait,1,2240,639
2026-11-03T07:11:27.1958524Z,arrow,1,320,659
2026-11-03T07:11:27.5088735Z,ibeam,1,959,659
2026-11-03T07:11:27.7258762Z,sizewe,1,1600,659
2026-11-03T07:11:28.0758556Z,wait,1,2240,659
2026-11-03T07:11:28.3948537Z,arrow,1,320,679
2026-11-03T07:11:28.6093549Z,ibeam,1,959,679
2026-11-03T07:11:28.9348850Z,sizewe,1,1600,679
2026-11-03T07:11:29.1518438Z,wait,1,2240,679
2026-11-03T07:11:29.4773532Z,arrow,1,320,699
2026-11-03T07:11:29.8318419Z,ibeam,1,959,699
2026-11-03T07:11:30.1438730Z,sizewe,1,1600,699
2026-11-03T07:11:30.3608443Z,wait,1,2240,699
2026-11-03T07:11:30.6863784Z,arrow,1,320,720
2026-11-03T07:11:30.9033588Z,ibeam,1,959,720
2026-11-03T07:11:31.2288704Z,sizewe,1,1600,720
2026-11-03T07:11:31.5543568Z,wait,1,2240,720
2026-11-03T07:11:31.8998470Z,arrow,1,320,740
2026-11-03T07:11:32.1123740Z,ibeam,1,959,740
2026-11-03T07:11:32.4378716Z,sizewe,1,1600,740
2026-11-03T07:11:32.7633726Z,wait,1,2240,740

View File

@ -0,0 +1,74 @@
{"version":1,"shape":"wait","visible":true,"x":2308,"y":758,"utc":"2026-11-03T07:07:02.9393420Z"}
utc,x,y,shape
2026-11-03T07:04:27.5718439Z,320,600,arrow
2026-11-03T07:04:27.8688460Z,959,600,ibeam
2026-11-03T07:04:28.1413412Z,1600,600,sizewe
2026-11-03T07:04:28.4368405Z,2240,600,wait
2026-11-03T07:04:28.7398485Z,320,620,arrow
2026-11-03T07:04:29.0323472Z,959,620,ibeam
2026-11-03T07:04:29.3253400Z,1600,620,sizewe
2026-11-03T07:04:29.6178424Z,2240,620,wait
2026-11-03T07:04:29.9088496Z,320,639,arrow
2026-11-03T07:04:30.2033461Z,959,639,ibeam
2026-11-03T07:04:30.4853411Z,1600,639,sizewe
2026-11-03T07:04:30.7708577Z,2240,639,wait
2026-11-03T07:04:31.0543463Z,320,659,arrow
2026-11-03T07:04:31.3768397Z,959,659,ibeam
2026-11-03T07:04:31.6688468Z,1600,659,sizewe
2026-11-03T07:04:31.9598392Z,2240,659,wait
2026-11-03T07:04:32.2533467Z,320,679,arrow
2026-11-03T07:04:32.5598422Z,959,679,ibeam
2026-11-03T07:04:32.8513466Z,1600,679,sizewe
2026-11-03T07:04:33.1398470Z,2240,679,wait
2026-11-03T07:04:33.4373390Z,320,699,arrow
2026-11-03T07:04:33.7618453Z,959,699,ibeam
2026-11-03T07:04:34.0528411Z,1600,699,sizewe
2026-11-03T07:04:34.3553395Z,2240,699,wait
2026-11-03T07:04:34.6193461Z,320,720,arrow
2026-11-03T07:04:34.9018406Z,959,720,ibeam
2026-11-03T07:04:35.1888416Z,1600,720,sizewe
2026-11-03T07:04:35.4698436Z,2240,720,wait
2026-11-03T07:04:35.7783469Z,320,740,arrow
2026-11-03T07:04:36.0693463Z,959,740,ibeam
2026-11-03T07:04:36.3558408Z,1600,740,sizewe
2026-11-03T07:04:36.6408493Z,2240,740,wait
utc,shape,flags,x,y
2026-11-03T07:04:11.1088554Z,wait,1,2426,267
2026-11-03T07:04:20.5148709Z,arrow,1,320,499
2026-11-03T07:04:23.8008747Z,ibeam,1,959,499
2026-11-03T07:04:24.4518732Z,sizewe,1,1600,499
2026-11-03T07:04:25.1028711Z,wait,1,2240,499
2026-11-03T07:04:25.7693710Z,unknown,0,320,1250
2026-11-03T07:04:26.4203719Z,arrow,1,320,499
2026-11-03T07:04:27.8463737Z,ibeam,1,959,600
2026-11-03T07:04:28.1718717Z,sizewe,1,1600,600
2026-11-03T07:04:28.3888745Z,wait,1,2240,600
2026-11-03T07:04:28.7143746Z,arrow,1,320,620
2026-11-03T07:04:29.0463438Z,ibeam,1,959,620
2026-11-03T07:04:29.2568716Z,sizewe,1,1600,620
2026-11-03T07:04:29.5978717Z,wait,1,2240,620
2026-11-03T07:04:29.9258382Z,arrow,1,320,639
2026-11-03T07:04:30.1403684Z,ibeam,1,959,639
2026-11-03T07:04:30.4658710Z,sizewe,1,1600,639
2026-11-03T07:04:30.7918446Z,wait,1,2240,639
2026-11-03T07:04:31.0083608Z,arrow,1,320,659
2026-11-03T07:04:31.3493702Z,ibeam,1,959,659
2026-11-03T07:04:31.6833398Z,sizewe,1,1600,659
2026-11-03T07:04:31.8918706Z,wait,1,2240,659
2026-11-03T07:04:32.2173716Z,arrow,1,320,679
2026-11-03T07:04:32.5428703Z,ibeam,1,959,679
2026-11-03T07:04:32.8683855Z,sizewe,1,1600,679
2026-11-03T07:04:33.0853744Z,wait,1,2240,679
2026-11-03T07:04:33.4263715Z,arrow,1,320,699
2026-11-03T07:04:33.7518751Z,ibeam,1,959,699
2026-11-03T07:04:34.0773750Z,sizewe,1,1600,699
2026-11-03T07:04:34.2943515Z,wait,1,2240,699
2026-11-03T07:04:34.6348401Z,arrow,1,320,720
2026-11-03T07:04:34.8368725Z,ibeam,1,959,720
2026-11-03T07:04:35.1778728Z,sizewe,1,1600,720
2026-11-03T07:04:35.3933486Z,wait,1,2240,720
2026-11-03T07:04:35.7203735Z,arrow,1,320,740
2026-11-03T07:04:36.0458710Z,ibeam,1,959,740
2026-11-03T07:04:36.3713767Z,sizewe,1,1600,740
2026-11-03T07:04:36.5883715Z,wait,1,2240,740

View File

@ -0,0 +1 @@
{"width": 32, "height": 32, "hotX": 8, "hotY": 9, "x": 960, "y": 500, "argb": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 4294112499, 4294112499, 0, 4294112499, 4294112499, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4294112499, 4294112499, 4294112499, 0, 4294112499, 4294112499, 4294112499, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}

View File

@ -0,0 +1 @@
{"version": 1, "shape": "arrow", "visible": true, "expires": 1793689894.0310185}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -0,0 +1,48 @@
[
{
"shape": "arrow",
"x": 320,
"y": 500,
"visible": true
},
{
"shape": "ibeam",
"x": 960,
"y": 500,
"visible": true
},
{
"shape": "sizewe",
"x": 1600,
"y": 500,
"visible": true
},
{
"shape": "wait",
"x": 2240,
"y": 500,
"visible": true
},
{
"shape": "__hidden",
"x": 320,
"y": 1250,
"visible": false
},
{
"test": "client-paused-local-motion",
"before": [
320,
500
],
"after": [
500,
650
],
"passed": true
},
{
"test": "helper-lease-expiry",
"passed": true
}
]

View File

@ -0,0 +1 @@
{"width": 32, "height": 32, "hotX": 11, "hotY": 4, "x": 1600, "y": 500, "argb": [0, 0, 0, 4278848013, 4278387726, 4278387726, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278387726, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278848013, 4278387726, 4294967295, 4278190093, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278190093, 4293980403, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278848013, 4278387726, 4294967295, 4294967295, 4278190093, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278190093, 4294967295, 4293717230, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278848013, 4278387726, 4294967295, 4294967295, 4294967295, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4294967295, 4294967295, 4293454058, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278387726, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4294967295, 4293454058, 4278387726, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278848013, 4278387726, 4294967295, 4294967295, 4294967295, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4278387726, 4294967295, 4294967295, 4293454058, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278848013, 4278387726, 4294967295, 4294967295, 4278190093, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278190093, 4294967295, 4293454058, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278848013, 4278387726, 4294967295, 4278190093, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278190093, 4293454058, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278848013, 4278387726, 4278387726, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4278387726, 4278387726, 4278848013, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}

View File

@ -0,0 +1,42 @@
{
"01-remote-cursor": {
"bright_pixels": 77,
"bbox": [
295,
474,
380,
559
],
"pixels_above_background_threshold": 77
},
"02-video-cursor-disabled": {
"bright_pixels": 0,
"bbox": [
296,
474,
381,
559
],
"pixels_above_background_threshold": 0
},
"03-local-mode-video-only": {
"bright_pixels": 0,
"bbox": [
2215,
715,
2300,
800
],
"pixels_above_background_threshold": 0
},
"04-remote-cursor-restored": {
"bright_pixels": 0,
"bbox": [
2215,
716,
2300,
801
],
"pixels_above_background_threshold": 226
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
# 独立光标二次开发设计与接入条件
状态:源码核实完成,尚未实现 Sunshine/Moonlight 的独立光标通道,不应标记为已部署。当前保留单个 Windows 远程白色光标。
状态:已通过受控的 Windows 光标导出 + 盒子 X11 本地指针原型验证;尚未实现 Sunshine/Moonlight 的自动独立光标通道,不应标记为生产功能已部署。测试已结束,当前恢复单个 Windows 远程白色光标。实测结果见 [独立光标可行性验证](independent-cursor-feasibility-20261103.md)。
## 已核实的实际路径

View File

@ -0,0 +1,53 @@
# 独立光标原型可行性实测(2026-11-03)
## 结论和当前现场
本地独立移动和 Windows 光标静态形状显示可行,值得继续开发 Sunshine + Moonlight 的正式同步协议。本轮完成了真实盒子和 VM100 的受控原型,不是仅做源码调研。但是自动形状同步、视频帧交接、断线回退尚未成为产品功能,不能默认启用,也不能称鼠标或窗口零延迟。
测试已结束,Windows 临时任务 VDICursorPOC 已停止并删除。正常 Moonlight PID31694、2560×1440/45FPS、RKMPP/EGL 和原二进制 SHA256 `35de7b1a4ade583823b23b5f2e012307f6d0a3848c8eb97bd6d4de9660afa00f` 保留。VDD读回 DISPLAY3、2560×1440@60。最后确认本地指针为全透明1×1,屏幕里只有正常Windows远程白色光标;没有遗留测试辅助进程。GTX965M错误码0,编码占用11%,Sunshine该正常会话创建了h264_nvenc。没有重启机器、修改GPU固件/直通、Windows鼠标主题或正式客户端配置。
## 原型做了什么
Windows已登录用户会话运行临时测试窗口,用 GetCursorInfo 读取实际光标状态;GetIconInfo/DrawIconEx 导出默认箭头、文字、左右缩放、忙碌四种真实Windows形状,均为32×32,热点分别为(0,0)、(8,9)、(11,4)、(16,16)。导出仅复制资源并释放 GetIconInfo 返回的位图,没有替换系统主题。通过黑白背景双次绘制重建alpha,检测到依赖背景反色的像素就拒绝;这四个样本未检测到此类像素,不代表全部自定义光标都支持。
管理工作站通过已有的认证PVE guest-agent通道读取导出文件,再经SSH传给盒子。没有新增网络监听端口,盒子没有直接使用PVE凭据。盒子辅助程序在现有Moonlight的X11窗口上创建本地指针,形状由测试脚本按已知测试区域顺序切换。**这是脚本控制的形状回放;没有将GetCursorInfo状态自动、实时同步到客户端。** Windows另存状态变化记录,供交叉核对。
测试临时使用Sunshine的Ctrl+Alt+Shift+N隐藏视频光标,最后成对恢复。它只是原型手段,没有会话确认/租约,不能作为正式交接协议。正常Moonlight二进制未重编或替换;本轮验证的是底层X11本地指针可用性。正式功能应进入Moonlight的SDL主线程,并适配后续Weston/Wayland,不能把这份X11辅助程序当最终架构。
## 实测结果
| 验证项 | 结果与边界 |
| --- | --- |
| 四种形状及热点 | XFixes读取本地指针,尺寸/热点均与Windows导出一致;忙碌只验证一帧,未实现动画 |
| 光标显隐 | 本地隐藏状态为全透明1×1;Windows记录也捕获到隐藏flags=0 |
| 移动独立于视频 | 仅将当前Moonlight进程短暂停住,小于0.5秒,并保证SIGCONT恢复;本地指针仍从(320,500)移动到(500,650),图像保持一致。这证明依赖关系解除,没有测得物理输入到屏幕毫秒数 |
| 单光标受控稳态 | 原始XWD截图在禁用后不再包含远程指针,XFixes另行确认本地指针存在。XWD不包含本地硬件指针,不能将原始截图无指针误报为本地显示失败;未用合成图冒充屏幕截图 |
| 点击正确性 | 最后完整一轮32次跨四区点击,Windows记录32/32区域及形状正确;绝对坐标归一化差值最大1像素,不宣称零偏差 |
| 辅助状态超时 | 0.8秒测试租约过期,辅助程序退出并通过SDL恢复其拥有的隐藏指针;测试控制器随后恢复远程视频光标。没有证明真实控制断网时服务器能自动回退 |
| 输入数据边界 | 10项测试通过,覆盖尺寸、热点、像素长度/范围、重复形状、XOR拒绝、格式、未知形状、过期/异常租约及文件大小 |
最后完整轮次为 `cursor-poc-evidence-v3`。最初两轮已验证形状、移动和点击,但辅助程序退出后当前XFixes图像暂不可用,不能算完整通过;修正后才通过完整流程。没有把前两轮的失败隐藏成成功。
## 暴露的两个实际问题
1. **静止画面的旧光标会留在旧视频帧里。** 第一轮切换快捷键后立即截图,原位置的远程箭头仍在;之后画面更新才消失。最后一轮加入测试窗口时钟更新并等待新帧,禁用后的光标区域变为空背景,恢复后的区域重新出现Windows忙碌光标。正式实现必须用双方确认的视频帧边界切换,不能只延时几百毫秒后直接显示本地箭头。
2. **X11指针资源有连接所有权。** 辅助程序在自己连接上复制一个隐藏指针,再关闭连接,并不足以可靠恢复SDL原先的资源。原型现在在XFixes临时隐藏保护下,使用Moonlight本地光标快捷键成对切换,让SDL重新设置它自己拥有的隐藏指针,再释放辅助资源。此清理依赖同一前台绝对鼠标会话,不能代替正式客户端内部的资源生命周期管理。代码失焦时明确报错,不声称任意退出路径都能可靠恢复。
## 下一阶段执行顺序
1. 固定现场Sunshine版本对应提交,在独立目录准备Windows可复现构建;当前未准备好该服务端C++构建环境。先验证未经修改的构建,再备份/部署测试实例,避免直接替换正常服务。
2. 在现有已认证、加密的控制通道添加能力协商、形状/热点、显隐、状态序号与显示配置代数。Moonlight将有界状态投递SDL主线程,缓存形状,本地移动直接跟随物理输入;旧服务端保持远程模式。
3. Sunshine提供有明确目标状态、帧边界确认和会话租约的光标交接,代替全局toggle;未捕获到可分离光标或形状不支持时回退。锁屏/UAC、远程主动warp、动画、缩放、多会话也须覆盖。
4. 验收断网/重连/失焦、文字选择、拖窗、右键/双击及100%/125%/150%缩放,再用高速摄像分别测指针移动、点击反馈、窗口内容的P50/P95。通过后再开放可关闭的试用开关。
独立光标可以绕开捕获、编码、网络、解码对“指针位置显示”的等待;窗口本身和点击后的页面仍需视频返回。它不会直接解决当前盒子2K60只实渲染约46FPS的问题,视频呈现优化仍按此前对照结论推进。
## 文件与复核
- `tools/cursor-poc-host.ps1`、`tools/run-cursor-poc-host.py`:临时Windows交互任务的start/status/export/stop。不自动触发、不修改Sunshine。
- `tools/cursor-poc-x11.py`:有界资产/状态解析、静态alpha形状、本地租约、SDL所有权归还;必须显式选择同一个前台Moonlight绝对鼠标窗口。仅用于受控实验。
- `tools/verify-cursor-poc-box.py`:受控演示、32点击、短暂停客户端及恢复;**仅可在上述Windows测试窗口位于前台时运行,不能向用户普通桌面盲目注入点击。** 输出目录拒绝覆盖,复跑须使用新目录名。
- `tools/test-cursor-poc.py`:Linux边界测试,不更改指针。
- `docs/evidence/cursor-poc-20261103/`:Windows资产和状态/点击记录、XFixes形状、原始截图转换、首轮失败证据、最终成功结果、超时日志、GPU/显示/本地指针收尾查询。压缩包保留原始XWD。
实现依据:[Microsoft GetCursorInfo](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getcursorinfo)、[GetIconInfo](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-geticoninfo)、[Desktop Duplication光标元数据](https://learn.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api)。正式协议及验收要求继续见 [双方二次开发设计](independent-cursor-design-20261103.md)。

122
tools/cursor-poc-host.ps1 Normal file
View File

@ -0,0 +1,122 @@
# Scoped interactive cursor probe. No theme, driver, display or Sunshine changes.
$ErrorActionPreference='Stop'
$folder='C:\ProgramData\VDI\cursor-poc-20261103'
Set-Content "$folder\pid.txt" $PID
try {
Add-Type -ReferencedAssemblies System.Windows.Forms,System.Drawing -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public class VDICursorShape {
public string name; public int width,height,hotX,hotY,invertedPixels;
public bool monochrome; public uint[] argb;
}
public class VDICursorProbe : Form {
[StructLayout(LayoutKind.Sequential)] struct POINT { public int x,y; }
[StructLayout(LayoutKind.Sequential)] struct CI { public int size,flags; public IntPtr cursor; public POINT point; }
[StructLayout(LayoutKind.Sequential)] struct II { public bool icon; public int x,y; public IntPtr mask,color; }
[DllImport("user32.dll")] static extern bool GetCursorInfo(ref CI ci);
[DllImport("user32.dll")] static extern bool GetIconInfo(IntPtr h,out II info);
[DllImport("user32.dll")] static extern bool DrawIconEx(IntPtr dc,int x,int y,IntPtr icon,int w,int h,int step,IntPtr brush,int flags);
[DllImport("gdi32.dll")] static extern bool DeleteObject(IntPtr h);
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
public static readonly string[] Names={"arrow","ibeam","sizewe","wait"};
static readonly Cursor[] Shapes={Cursors.Default,Cursors.IBeam,Cursors.SizeWE,Cursors.WaitCursor};
static Bitmap Draw(IntPtr handle,int w,int h,Color background) {
Bitmap result=new Bitmap(w,h,System.Drawing.Imaging.PixelFormat.Format32bppArgb);
using(Graphics g=Graphics.FromImage(result)) {
g.Clear(background); IntPtr dc=g.GetHdc();
try { if(!DrawIconEx(dc,0,0,handle,w,h,0,IntPtr.Zero,3)) throw new Exception("DrawIconEx failed"); }
finally { g.ReleaseHdc(dc); }
}
return result;
}
public static VDICursorShape Export(string name,IntPtr handle) {
II info; if(!GetIconInfo(handle,out info)) throw new Exception("GetIconInfo failed");
try {
int w,h;
using(Bitmap mask=Bitmap.FromHbitmap(info.mask)) { w=mask.Width; h=mask.Height/(info.color==IntPtr.Zero?2:1); }
if(w<1 || h<1 || w>128 || h>128) throw new Exception("Unsupported cursor dimensions");
VDICursorShape s=new VDICursorShape {name=name,width=w,height=h,hotX=info.x,hotY=info.y,monochrome=info.color==IntPtr.Zero,argb=new uint[w*h]};
using(Bitmap black=Draw(handle,w,h,Color.Black))
using(Bitmap white=Draw(handle,w,h,Color.White)) {
for(int y=0;y<h;y++) for(int x=0;x<w;x++) {
Color b=black.GetPixel(x,y), z=white.GetPixel(x,y);
int dr=z.R-b.R,dg=z.G-b.G,db=z.B-b.B;
// XOR/inverted cursors need destination-dependent composition.
// Preserve the unsupported count; never silently call them alpha cursors.
if(dr<0 || dg<0 || db<0 || Math.Abs(dr-dg)>2 || Math.Abs(dr-db)>2) { ++s.invertedPixels; continue; }
int a=255-dr;
if(a==0) continue;
uint r=(uint)Math.Min(255,(b.R*255+a/2)/a),g=(uint)Math.Min(255,(b.G*255+a/2)/a),bl=(uint)Math.Min(255,(b.B*255+a/2)/a);
s.argb[y*w+x]=((uint)a<<24)|(r<<16)|(g<<8)|bl;
}
}
return s;
} finally { if(info.mask!=IntPtr.Zero) DeleteObject(info.mask); if(info.color!=IntPtr.Zero) DeleteObject(info.color); }
}
public static VDICursorShape[] ExportAll() {
List<VDICursorShape> result=new List<VDICursorShape>();
for(int i=0;i<Names.Length;i++) result.Add(Export(Names[i],Shapes[i].Handle));
return result.ToArray();
}
readonly Timer timer=new Timer(); readonly string folder; readonly DateTime started=DateTime.UtcNow;
bool hidden; string shape="arrow",lastObserved=""; int clicks;
public VDICursorProbe(string path) {
folder=path; Text="VDI Independent Cursor Probe"; FormBorderStyle=FormBorderStyle.None;
Bounds=Screen.PrimaryScreen.Bounds; TopMost=true; DoubleBuffered=true;
File.WriteAllText(Path.Combine(folder,"clicks.csv"),"utc,x,y,shape\n");
File.WriteAllText(Path.Combine(folder,"states.csv"),"utc,shape,flags,x,y\n");
timer.Interval=100;
timer.Tick+=delegate {
if((DateTime.UtcNow-started).TotalMinutes>8) { Close(); return; }
CI ci=new CI(); ci.size=Marshal.SizeOf(typeof(CI));
if(!GetCursorInfo(ref ci)) return;
string observed="unknown";
for(int i=0;i<Shapes.Length;i++) if(ci.cursor==Shapes[i].Handle) observed=Names[i];
string observedKey=observed+","+ci.flags;
if(observedKey!=lastObserved) {
File.AppendAllText(Path.Combine(folder,"states.csv"),DateTime.UtcNow.ToString("o")+","+observedKey+","+ci.point.x+","+ci.point.y+"\n");
lastObserved=observedKey;
}
string json="{\"version\":1,\"shape\":\""+observed+"\",\"visible\":"+(((ci.flags&1)!=0)?"true":"false")+",\"x\":"+ci.point.x+",\"y\":"+ci.point.y+",\"utc\":\""+DateTime.UtcNow.ToString("o")+"\"}";
string target=Path.Combine(folder,"state.json"),temp=target+".new";
File.WriteAllText(temp,json);
if(File.Exists(target)) File.Replace(temp,target,null); else File.Move(temp,target);
Invalidate(new Rectangle(0,0,Width,140));
};
timer.Start();
}
protected override void OnMouseMove(MouseEventArgs e) {
base.OnMouseMove(e);
bool nextHidden=e.Y>Height*3/4;
if(nextHidden!=hidden) { if(nextHidden) Cursor.Hide(); else Cursor.Show(); hidden=nextHidden; }
int index=Math.Min(3,e.X*4/Math.Max(1,Width)); shape=Names[index]; Cursor=Shapes[index];
}
protected override void OnMouseDown(MouseEventArgs e) {
base.OnMouseDown(e); ++clicks;
File.AppendAllText(Path.Combine(folder,"clicks.csv"),DateTime.UtcNow.ToString("o")+","+e.X+","+e.Y+","+shape+"\n"); Invalidate();
}
protected override void OnPaint(PaintEventArgs e) {
e.Graphics.Clear(Color.FromArgb(24,32,44));
using(Font f=new Font("Consolas",23)) for(int i=0;i<4;i++) {
e.Graphics.DrawRectangle(Pens.SlateGray,i*Width/4,150,Width/4-2,Height*3/4-150);
e.Graphics.DrawString(Names[i],f,Brushes.White,i*Width/4+40,200);
}
using(Font f=new Font("Consolas",24)) {
e.Graphics.DrawString("Independent cursor probe | clicks: "+clicks+" | "+DateTime.UtcNow.ToString("HH:mm:ss.fff"),f,Brushes.White,40,40);
e.Graphics.DrawString("BOTTOM AREA: cursor hidden",f,Brushes.Gold,40,Height*3/4+60);
}
}
protected override void OnFormClosed(FormClosedEventArgs e) {
timer.Stop(); timer.Dispose(); if(hidden) { Cursor.Show(); hidden=false; } base.OnFormClosed(e);
}
}
'@
[VDICursorProbe]::SetProcessDPIAware() | Out-Null
@{version=1; shapes=[VDICursorProbe]::ExportAll()} | ConvertTo-Json -Depth 6 -Compress | Set-Content -Encoding utf8 "$folder\shapes.json"
[System.Windows.Forms.Application]::Run((New-Object VDICursorProbe($folder)))
} catch { $_ | Out-String | Set-Content "$folder\error.txt"; throw }

223
tools/cursor-poc-x11.py Normal file
View File

@ -0,0 +1,223 @@
#!/usr/bin/env python3
"""Temporary X11 local-cursor proof, NOT a production Moonlight cursor protocol.
Requires an already authenticated Moonlight session. Never changes Sunshine.
The caller owns remote-video-cursor handoff and must restore it in finally.
Only supports exported alpha cursors without destination-dependent XOR pixels.
"""
import argparse
import ctypes as C
import fcntl
import json
import math
import os
from pathlib import Path
import signal
import subprocess
import time
class XcursorImage(C.Structure):
_fields_ = [("version", C.c_uint32), ("size", C.c_uint32),
("width", C.c_uint32), ("height", C.c_uint32),
("xhot", C.c_uint32), ("yhot", C.c_uint32),
("delay", C.c_uint32), ("pixels", C.POINTER(C.c_uint32))]
class XfixesImage(C.Structure):
_fields_ = [("x", C.c_short), ("y", C.c_short),
("width", C.c_ushort), ("height", C.c_ushort),
("xhot", C.c_ushort), ("yhot", C.c_ushort),
("serial", C.c_ulong), ("pixels", C.POINTER(C.c_ulong)),
("atom", C.c_ulong), ("name", C.c_char_p)]
def validate_assets(data):
if not isinstance(data, dict) or type(data.get("version")) is not int or data.get("version") != 1 or not isinstance(data.get("shapes"), list) or not 1 <= len(data["shapes"]) <= 32:
raise ValueError("Invalid asset version/count")
result = {}
for s in data["shapes"]:
if not isinstance(s, dict):
raise ValueError("Invalid shape")
name = s.get("name")
if not isinstance(name, str) or not name.isascii() or not name.isalnum() or len(name) > 32 or name in result:
raise ValueError("Invalid/duplicate shape name")
for key in ("width", "height", "hotX", "hotY", "invertedPixels"):
if type(s.get(key)) is not int:
raise ValueError("Noninteger cursor metadata")
w, h = s["width"], s["height"]
if not 1 <= w <= 128 or not 1 <= h <= 128 or not 0 <= s["hotX"] < w or not 0 <= s["hotY"] < h:
raise ValueError("Invalid size/hotspot")
if s["invertedPixels"] != 0:
raise ValueError("Destination-dependent XOR cursor requires fallback")
pixels = s.get("argb")
if not isinstance(pixels, list) or len(pixels) != w*h or any(type(p) is not int or not 0 <= p <= 0xffffffff for p in pixels):
raise ValueError("Invalid ARGB pixels")
result[name] = s
return result
def validate_state(state, shapes):
if not isinstance(state, dict) or type(state.get("version")) is not int or state.get("version") != 1 or type(state.get("visible")) is not bool or not isinstance(state.get("shape"), str) or state.get("shape") not in shapes:
raise ValueError("Invalid state/unknown shape")
# A bounded wall-clock lease prevents stale local pointers after the bridge dies.
expires = state.get("expires")
if type(expires) not in (int, float) or not math.isfinite(expires) or not time.time() < expires <= time.time()+5:
raise ValueError("Expired/invalid state lease")
return state["shape"] if state["visible"] else "__hidden"
def read_json(path, limit):
with open(path, "rb") as f:
raw = f.read(limit+1)
if len(raw) > limit:
raise ValueError("Oversize JSON")
return json.loads(raw)
class LocalCursor:
def __init__(self, window):
self.x = C.CDLL("libX11.so.6")
self.c = C.CDLL("libXcursor.so.1")
self.f = C.CDLL("libXfixes.so.3")
self.x.XOpenDisplay.argtypes = [C.c_char_p]; self.x.XOpenDisplay.restype = C.c_void_p
self.x.XDefineCursor.argtypes = [C.c_void_p, C.c_ulong, C.c_ulong]
self.x.XSync.argtypes = [C.c_void_p, C.c_int]
self.x.XFree.argtypes = [C.c_void_p]
self.x.XFreeCursor.argtypes = [C.c_void_p, C.c_ulong]
self.x.XCloseDisplay.argtypes = [C.c_void_p]
self.f.XFixesGetCursorImage.argtypes = [C.c_void_p]; self.f.XFixesGetCursorImage.restype = C.POINTER(XfixesImage)
self.f.XFixesHideCursor.argtypes = [C.c_void_p, C.c_ulong]
self.f.XFixesShowCursor.argtypes = [C.c_void_p, C.c_ulong]
self.c.XcursorImageCreate.argtypes = [C.c_int, C.c_int]; self.c.XcursorImageCreate.restype = C.POINTER(XcursorImage)
self.c.XcursorImageDestroy.argtypes = [C.POINTER(XcursorImage)]
self.c.XcursorImageLoadCursor.argtypes = [C.c_void_p, C.POINTER(XcursorImage)]; self.c.XcursorImageLoadCursor.restype = C.c_ulong
self.d = self.x.XOpenDisplay(None)
if not self.d:
raise RuntimeError("Cannot open X display")
self.window = window
self.cursors = {}
self.original = self.create(self.snapshot(), premultiplied=True)
def snapshot(self):
image = self.f.XFixesGetCursorImage(self.d)
if not image:
raise RuntimeError("No XFixes cursor image")
try:
i = image.contents
if not 1 <= i.width <= 128 or not 1 <= i.height <= 128:
raise RuntimeError("Unsupported X cursor dimensions")
return dict(width=i.width, height=i.height, hotX=i.xhot, hotY=i.yhot,
x=i.x, y=i.y, argb=[int(i.pixels[p]) & 0xffffffff for p in range(i.width*i.height)])
finally:
self.x.XFree(image)
def create(self, s, premultiplied=False):
image = self.c.XcursorImageCreate(s["width"], s["height"])
if not image:
raise RuntimeError("Xcursor allocation failed")
try:
image.contents.xhot, image.contents.yhot = s["hotX"], s["hotY"]
for j, p in enumerate(s["argb"]):
if premultiplied:
image.contents.pixels[j] = p
continue
# XRender/Xcursor uses premultiplied alpha; Windows export is straight ARGB.
a = p >> 24
r, g, b = ((p >> shift) & 255 for shift in (16, 8, 0))
image.contents.pixels[j] = (a << 24) | ((r*a+127)//255 << 16) | ((g*a+127)//255 << 8) | ((b*a+127)//255)
cursor = self.c.XcursorImageLoadCursor(self.d, image)
if not cursor:
raise RuntimeError("Xcursor creation failed")
return cursor
finally:
self.c.XcursorImageDestroy(image)
def apply(self, cursor):
self.x.XDefineCursor(self.d, self.window, cursor)
self.x.XSync(self.d, 0)
def close(self, sdl_pid=None):
self.apply(self.original)
try:
if sdl_pid is not None:
# X11 cursor resources owned by this connection disappear when
# it closes. Have SDL restore its own hidden cursor first. The
# temporary XFixes hide prevents the first toggle flashing the
# default local arrow. This POC requires the same focused stream.
active = subprocess.check_output(["xdotool", "getwindowfocus", "getwindowpid"],text=True).strip()
if active != str(sdl_pid):
raise RuntimeError("Cannot return cursor ownership: stream lost focus; reconnect normal client")
self.f.XFixesHideCursor(self.d,self.window); self.x.XSync(self.d,0)
try:
for unused in range(2):
subprocess.run(["xdotool","key","ctrl+alt+shift+c"],check=True)
time.sleep(.08)
finally:
self.f.XFixesShowCursor(self.d,self.window); self.x.XSync(self.d,0)
finally:
for cursor in [self.original, *self.cursors.values()]:
self.x.XFreeCursor(self.d, cursor)
self.x.XCloseDisplay(self.d)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--window", type=int, required=True)
parser.add_argument("--pid", type=int, required=True)
parser.add_argument("--assets", type=Path, required=True)
parser.add_argument("--state", type=Path, required=True)
parser.add_argument("--seconds", type=int, default=120)
parser.add_argument("--snapshot", type=Path)
args = parser.parse_args()
if not 1 <= args.seconds <= 180 or args.window <= 0 or args.pid <= 1:
parser.error("Invalid window/pid/duration")
command = Path(f"/proc/{args.pid}/cmdline").read_bytes().replace(b"\0", b" ")
if b"/moonlight " not in command or b" stream " not in command or b" --absolute-mouse " not in command:
parser.error("Target must be the absolute-mode Moonlight stream")
actual_pid = subprocess.check_output(["xdotool", "getwindowpid", str(args.window)], text=True).strip()
focused_window = subprocess.check_output(["xdotool", "getwindowfocus"], text=True).strip()
if actual_pid != str(args.pid) or focused_window != str(args.window):
parser.error("Target must be the same focused Moonlight window")
# Keep the experiment exclusive, with owner-only local files.
lock = open(args.state.with_suffix(".lock"), "w")
os.chmod(lock.name, 0o600)
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
shapes = validate_assets(read_json(args.assets, 4*1024*1024))
cursor = LocalCursor(args.window)
if any(cursor.snapshot()["argb"]):
cursor.x.XFreeCursor(cursor.d, cursor.original)
cursor.x.XCloseDisplay(cursor.d)
parser.error("POC requires the original local cursor to be hidden")
stop = False
def stopped(*unused):
nonlocal stop
stop = True
signal.signal(signal.SIGTERM, stopped); signal.signal(signal.SIGINT, stopped)
try:
cursor.cursors = {name: cursor.create(s) for name, s in shapes.items()}
cursor.cursors["__hidden"] = cursor.create(dict(width=1, height=1, hotX=0, hotY=0, argb=[0]))
current = None
end = time.monotonic()+args.seconds
while not stop and time.monotonic() < end:
if not Path(f"/proc/{args.pid}").exists():
break
try:
desired = validate_state(read_json(args.state, 1024), shapes)
except (ValueError, OSError) as e:
print(json.dumps(dict(event="lease-ended", reason=str(e))), flush=True)
break
if desired != current:
cursor.apply(cursor.cursors[desired]); current = desired
s = cursor.snapshot()
if args.snapshot:
args.snapshot.write_text(json.dumps(s))
print(json.dumps(dict(event="applied", shape=current, x=s["x"], y=s["y"], width=s["width"], height=s["height"], hotX=s["hotX"], hotY=s["hotY"])), flush=True)
time.sleep(.01)
finally:
cursor.close(sdl_pid=args.pid)
print('{"event":"sdl-hidden-cursor-restored"}', flush=True)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,73 @@
"""Scoped VM100 interactive cursor exporter. No new network listener or system cursor theme."""
import argparse
import base64
import json
from pathlib import Path
import runpy
import sys
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("action", choices=["start", "status", "export", "stop"])
parser.add_argument("--output", type=Path)
args = parser.parse_args()
root = Path(__file__).resolve().parent
t = runpy.run_path(str(root / "pve_term_exec.py"))
v = runpy.run_path(str(root / "pve_vm.py"))
ticket, csrf = t["login"]()
if v["api"]("/api2/json/nodes/starhe/qemu/100/status/current", ticket=ticket).get("status") != "running":
sys.exit("VM100 is not running")
command = r"$ErrorActionPreference='Stop'; $folder='C:\ProgramData\VDI\cursor-poc-20261103'; $task='VDICursorPOC'; "
if args.action == "start":
payload = base64.b64encode((root / "cursor-poc-host.ps1").read_bytes()).decode()
command += r"""
if(Get-ScheduledTask -TaskName $task -ErrorAction SilentlyContinue) { throw 'Probe already exists' }
$user=(Get-CimInstance Win32_ComputerSystem).UserName
if(-not $user) { throw 'No interactive user' }
New-Item -ItemType Directory -Force $folder | Out-Null
$acl=Get-Acl $folder
$acl.SetAccessRule((New-Object Security.AccessControl.FileSystemAccessRule($user,'Modify','ContainerInherit,ObjectInherit','None','Allow')))
Set-Acl $folder $acl
Remove-Item "$folder\error.txt","$folder\state.json","$folder\shapes.json" -ErrorAction SilentlyContinue
[IO.File]::WriteAllBytes("$folder\probe.ps1",[Convert]::FromBase64String('__PAYLOAD__'))
$a=New-ScheduledTaskAction -Execute powershell.exe -Argument "-NoProfile -STA -ExecutionPolicy Bypass -WindowStyle Hidden -File $folder\probe.ps1"
$p=New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited
$s=New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 9)
Register-ScheduledTask -TaskName $task -Action $a -Principal $p -Settings $s | Out-Null
Start-ScheduledTask -TaskName $task
Start-Sleep -Seconds 3
if(Test-Path "$folder\error.txt") { throw (Get-Content "$folder\error.txt" -Raw) }
if(-not(Test-Path "$folder\shapes.json")) { throw 'No fresh cursor export' }
'Cursor probe started'
""".replace("__PAYLOAD__", payload)
elif args.action == "export":
command += 'Get-Content "$folder\\shapes.json" -Raw'
elif args.action == "status":
command += r"""
Get-Content "$folder\state.json" -Raw
Get-Content "$folder\clicks.csv"
Get-Content "$folder\states.csv"
Get-Content "$folder\error.txt" -ErrorAction SilentlyContinue
"""
else:
command += r"""
Stop-ScheduledTask -TaskName $task -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue
if(Test-Path "$folder\pid.txt") {
$probePid=[int](Get-Content "$folder\pid.txt")
$process=Get-CimInstance Win32_Process -Filter "ProcessId=$probePid"
if($process.CommandLine -like '*cursor-poc-20261103*') { Stop-Process -Id $probePid -ErrorAction SilentlyContinue }
}
'Cursor probe stopped'
"""
out, error = v["agent"](ticket, csrf, ["powershell.exe", "-NoProfile", "-Command", command])
if error or not out or out == "TIMEOUT":
sys.exit(error or out or "Empty guest-agent result")
if args.action == "export":
data = json.loads(out.lstrip("\ufeff"))
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(data, separators=(",", ":")), encoding="utf-8")
print(json.dumps([{k: s[k] for k in ("name", "width", "height", "hotX", "hotY", "monochrome", "invertedPixels")} for s in data["shapes"]]))
else:
print(out)

50
tools/test-cursor-poc.py Normal file
View File

@ -0,0 +1,50 @@
"""Cursor asset boundary/fallback tests; run on Linux (same fcntl environment as box)."""
import copy
import importlib.util
from pathlib import Path
import tempfile
import time
import unittest
spec=importlib.util.spec_from_file_location("cursor_poc",Path(__file__).with_name("cursor-poc-x11.py"))
m=importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
class Boundaries(unittest.TestCase):
def setUp(self):
self.asset=dict(version=1,shapes=[dict(name="arrow",width=1,height=1,hotX=0,hotY=0,invertedPixels=0,argb=[0xffffffff])])
def bad(self,key,value):
data=copy.deepcopy(self.asset); data["shapes"][0][key]=value
with self.assertRaises(ValueError): m.validate_assets(data)
def test_valid(self): self.assertIn("arrow",m.validate_assets(self.asset))
def test_dimensions(self):
for v in (-1,0,129,True,"32"): self.bad("width",v)
def test_hotspot(self):
for v in (-1,1): self.bad("hotX",v)
def test_xor_requires_fallback(self): self.bad("invertedPixels",1)
def test_pixels(self):
for v in ([],[True],[-1],[0x100000000],[0,0]): self.bad("argb",v)
def test_duplicate_shapes(self):
self.asset["shapes"]*=2
with self.assertRaises(ValueError): m.validate_assets(self.asset)
def test_envelopes(self):
for v in (None,[],{},dict(version=True,shapes=self.asset["shapes"])):
with self.assertRaises(ValueError): m.validate_assets(v)
def test_lease(self):
shapes=m.validate_assets(self.asset)
for expires in (time.time()-1,time.time()+60,float("nan"),float("inf"),True):
with self.assertRaises(ValueError): m.validate_state(dict(version=1,visible=True,shape="arrow",expires=expires),shapes)
def test_state(self):
shapes=m.validate_assets(self.asset)
state=dict(version=1,visible=True,shape="arrow",expires=time.time()+1)
self.assertEqual(m.validate_state(state,shapes),"arrow")
state["visible"]=False
self.assertEqual(m.validate_state(state,shapes),"__hidden")
for key,value in (("visible",1),("shape","missing"),("shape",[]),("version",True)):
bad=dict(state); bad[key]=value
with self.assertRaises(ValueError): m.validate_state(bad,shapes)
def test_oversize_file(self):
with tempfile.TemporaryDirectory() as d:
p=Path(d)/"data.json"; p.write_text(" "*1025)
with self.assertRaises(ValueError): m.read_json(p,1024)
if __name__=="__main__": unittest.main()

View File

@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Controlled existing-stream POC; always restores the remote-video cursor toggle.
Only run with cursor-poc-host.ps1's disposable foreground window on Windows.
Shape selections here are scripted replay, NOT automatic host synchronization.
"""
import importlib.util
import json
import os
from pathlib import Path
import signal
import subprocess as S
import time
ROOT = Path(__file__).resolve().parent
os.environ.update(DISPLAY=":0", XAUTHORITY="/home/armbian/.Xauthority")
spec = importlib.util.spec_from_file_location("cursor_poc", ROOT / "cursor-poc-x11.py")
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
OUT = ROOT / "cursor-poc-evidence-v3"
OUT.mkdir(exist_ok=False) # Never overwrite earlier evidence.
def xd(*args):
return S.check_output(["xdotool", *map(str,args)], text=True).strip()
pid = int(xd("getwindowfocus", "getwindowpid"))
window = int(xd("getwindowfocus"))
cmd = Path(f"/proc/{pid}/cmdline").read_bytes().replace(b"\0", b" ")
if b"/moonlight " not in cmd or b" stream 192.168.1.70 Desktop " not in cmd or b" --absolute-mouse " not in cmd:
raise RuntimeError("Unexpected foreground process")
assets = m.validate_assets(m.read_json(ROOT / "windows-shapes.json", 4*1024*1024))
cursor = m.LocalCursor(window)
if any(cursor.snapshot()["argb"]):
cursor.close(); raise RuntimeError("Original local cursor is not hidden")
hidden_remote = False
paused = False
records = []
def interrupted(signum, frame):
raise SystemExit(128+signum)
signal.signal(signal.SIGTERM, interrupted)
signal.signal(signal.SIGINT, interrupted)
def focus():
if int(xd("getwindowfocus", "getwindowpid")) != pid:
raise RuntimeError("Moonlight lost foreground; stop the POC")
def capture(name):
with (OUT / (name+".xwd")).open("wb") as f:
S.run(["xwd", "-root", "-silent"], stdout=f, check=True)
try:
cursor.cursors = {k: cursor.create(v) for k,v in assets.items()}
cursor.cursors["__hidden"] = cursor.create(dict(width=1,height=1,hotX=0,hotY=0,argb=[0]))
focus(); xd("mousemove", 320, 500); time.sleep(.8)
capture("01-remote-cursor")
focus(); xd("key", "ctrl+alt+shift+n"); hidden_remote=True
xd("mousemove",321,500); time.sleep(1.0)
capture("02-video-cursor-disabled")
for name,x in (("arrow",320),("ibeam",960),("sizewe",1600),("wait",2240),("__hidden",320)):
focus(); y=1250 if name=="__hidden" else 500
xd("mousemove", x,y); time.sleep(.25)
cursor.apply(cursor.cursors[name]); time.sleep(.05)
snap=cursor.snapshot()
(OUT/(name+".json")).write_text(json.dumps(snap))
if name=="__hidden":
assert not any(snap["argb"]), "Hidden cursor still has visible pixels"
else:
a=assets[name]
assert (snap["width"],snap["height"],snap["hotX"],snap["hotY"]) == (a["width"],a["height"],a["hotX"],a["hotY"])
assert any(snap["argb"]), "Visible cursor has no pixels"
assert (snap["x"],snap["y"])==(x,y)
records.append(dict(shape=name,x=x,y=y,visible=any(snap["argb"])))
time.sleep(.3) # Allow the host probe to observe the real shape/visibility.
focus(); xd("mousemove",320,500); cursor.apply(cursor.cursors["arrow"]); time.sleep(.4)
# Freeze only this client for < 0.5s; X11 cursor must move while its video/input
# event processing is stopped. SIGCONT is guaranteed before cursor restoration.
os.kill(pid,signal.SIGSTOP); paused=True
try:
first=cursor.snapshot()
xd("mousemove",500,650); time.sleep(.12)
second=cursor.snapshot()
assert (second["x"],second["y"])==(500,650)
assert first["argb"]==second["argb"]
records.append(dict(test="client-paused-local-motion",before=[first["x"],first["y"]],after=[second["x"],second["y"]],passed=True))
finally:
os.kill(pid,signal.SIGCONT); paused=False
time.sleep(.5)
# Click only within the disposable Windows form. Scripted shape replay is
# deliberately distinct from the not-yet-implemented live cursor protocol.
for i in range(32):
focus(); n=i%4; x=320+640*n; y=600+(i//4)*20
cursor.apply(cursor.cursors[("arrow","ibeam","sizewe","wait")[n]])
xd("mousemove",x,y); time.sleep(.06); xd("click",1); time.sleep(.06)
capture("03-local-mode-video-only")
# Verify the actual helper's lease failure path while video cursor is hidden.
cursor.apply(cursor.original)
lease=OUT/"lease.json"
lease.write_text(json.dumps(dict(version=1,shape="arrow",visible=True,expires=time.time()+.8)))
result=S.run(["python3",str(ROOT/"cursor-poc-x11.py"),"--window",str(window),"--pid",str(pid),
"--assets",str(ROOT/"windows-shapes.json"),"--state",str(lease),"--seconds","3"],
capture_output=True,text=True,timeout=5)
(OUT/"lease-expiry.log").write_text(result.stdout+result.stderr)
assert result.returncode==0 and "lease-ended" in result.stdout and "sdl-hidden-cursor-restored" in result.stdout
# Xorg can briefly have no current cursor image after another connection's
# final cursor resource is released. Trigger pointer recomputation before
# checking the *post-exit* image; do not mistake this for an alpha image.
xd("mousemove",2240,741); time.sleep(.1)
assert not any(cursor.snapshot()["argb"])
records.append(dict(test="helper-lease-expiry",passed=True))
finally:
if paused: os.kill(pid,signal.SIGCONT)
try:
cursor.close(sdl_pid=pid)
finally:
# Remote restoration must still run when local cleanup raises an error.
if hidden_remote:
xd("windowactivate","--sync",window); focus()
xd("key","ctrl+alt+shift+n"); time.sleep(.8)
capture("04-remote-cursor-restored")
(OUT/"results.json").write_text(json.dumps(records,indent=2))
print(json.dumps(dict(restored_remote_cursor=hidden_remote,records=records)),flush=True)