验证:补充独立光标可行性POC
|
After Width: | Height: | Size: 7.0 KiB |
BIN
docs/evidence/cursor-poc-20261103/01-remote-cursor.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/evidence/cursor-poc-20261103/02-video-cursor-disabled.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
BIN
docs/evidence/cursor-poc-20261103/03-local-mode-video-only.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/evidence/cursor-poc-20261103/04-remote-cursor-restored.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
1
docs/evidence/cursor-poc-20261103/__hidden.json
Normal file
@ -0,0 +1 @@
|
||||
{"width": 1, "height": 1, "hotX": 0, "hotY": 0, "x": 320, "y": 1250, "argb": [0]}
|
||||
1
docs/evidence/cursor-poc-20261103/arrow.json
Normal 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]}
|
||||
BIN
docs/evidence/cursor-poc-20261103/before.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
@ -0,0 +1,5 @@
|
||||
{
|
||||
"clicks": 32,
|
||||
"correct_regions_and_shapes": 32,
|
||||
"max_coordinate_quantization_px": 1
|
||||
}
|
||||
BIN
docs/evidence/cursor-poc-20261103/cursor-poc-evidence-v3.tar.gz
Normal file
3
docs/evidence/cursor-poc-20261103/final-display.txt
Normal file
@ -0,0 +1,3 @@
|
||||
{"action":"Query","device":"\\\\.\\DISPLAY3","hz":60,"width":2560,"bpp":32,"height":1440}
|
||||
task_result=0
|
||||
|
||||
10
docs/evidence/cursor-poc-20261103/final-host.txt
Normal 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]
|
||||
|
||||
|
||||
|
||||
2
docs/evidence/cursor-poc-20261103/final-local-cursor.txt
Normal file
@ -0,0 +1,2 @@
|
||||
{'width': 1, 'height': 1, 'hotX': 0, 'hotY': 0, 'x': 100, 'y': 100}
|
||||
visible_local_pixels 0
|
||||
BIN
docs/evidence/cursor-poc-20261103/final-normal-desktop.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
docs/evidence/cursor-poc-20261103/first-01-remote-cursor.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 139 KiB |
1
docs/evidence/cursor-poc-20261103/first-__hidden.json
Normal file
@ -0,0 +1 @@
|
||||
{"width": 1, "height": 1, "hotX": 0, "hotY": 0, "x": 320, "y": 1250, "argb": [0]}
|
||||
1
docs/evidence/cursor-poc-20261103/first-arrow.json
Normal 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]}
|
||||
1
docs/evidence/cursor-poc-20261103/first-ibeam.json
Normal 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]}
|
||||
1
docs/evidence/cursor-poc-20261103/first-lease.json
Normal file
@ -0,0 +1 @@
|
||||
{"version": 1, "shape": "arrow", "visible": true, "expires": 1793689477.871138}
|
||||
44
docs/evidence/cursor-poc-20261103/first-results.json
Normal 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
|
||||
}
|
||||
]
|
||||
1
docs/evidence/cursor-poc-20261103/first-sizewe.json
Normal 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]}
|
||||
1
docs/evidence/cursor-poc-20261103/first-wait.json
Normal file
74
docs/evidence/cursor-poc-20261103/host-final-probe.txt
Normal 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
|
||||
|
||||
74
docs/evidence/cursor-poc-20261103/host-first.txt
Normal 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
|
||||
|
||||
1
docs/evidence/cursor-poc-20261103/ibeam.json
Normal 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]}
|
||||
1
docs/evidence/cursor-poc-20261103/lease.json
Normal file
@ -0,0 +1 @@
|
||||
{"version": 1, "shape": "arrow", "visible": true, "expires": 1793689894.0310185}
|
||||
BIN
docs/evidence/cursor-poc-20261103/remote-before-crop.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
docs/evidence/cursor-poc-20261103/remote-hidden-crop.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
48
docs/evidence/cursor-poc-20261103/results.json
Normal 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
|
||||
}
|
||||
]
|
||||
1
docs/evidence/cursor-poc-20261103/sizewe.json
Normal 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]}
|
||||
42
docs/evidence/cursor-poc-20261103/video-cursor-regions.json
Normal 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
|
||||
}
|
||||
}
|
||||
1
docs/evidence/cursor-poc-20261103/wait.json
Normal file
1
docs/evidence/cursor-poc-20261103/windows-shapes.json
Normal file
@ -1,6 +1,6 @@
|
||||
# 独立光标二次开发设计与接入条件
|
||||
|
||||
状态:源码核实完成,尚未实现 Sunshine/Moonlight 的独立光标通道,不应标记为已部署。当前保留单个 Windows 远程白色光标。
|
||||
状态:已通过受控的 Windows 光标导出 + 盒子 X11 本地指针原型验证;尚未实现 Sunshine/Moonlight 的自动独立光标通道,不应标记为生产功能已部署。测试已结束,当前恢复单个 Windows 远程白色光标。实测结果见 [独立光标可行性验证](independent-cursor-feasibility-20261103.md)。
|
||||
|
||||
## 已核实的实际路径
|
||||
|
||||
|
||||
53
docs/independent-cursor-feasibility-20261103.md
Normal 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
@ -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
@ -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()
|
||||
73
tools/run-cursor-poc-host.py
Normal 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
@ -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()
|
||||
120
tools/verify-cursor-poc-box.py
Normal 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)
|
||||