3 Commits

Author SHA1 Message Date
lirent 977c056287 feat: report on-screen cursor position in producer contract
Append cursor_seq/cursor_visible/cursor_pos to the producer block
(host-RO ABI; offsets 168/172/176, existing layout unchanged). Position
is reported every pump tick, independent of draw_cursor, so a consumer
can overlay its own cursor even when the producer does not composite it.

x and y pack into one 8-aligned 64-bit field (single atomic store, no
tear); cursor_seq bumps last to gate a consistent read. Adds the
vgpu_publish_cursor() engine setter and a lightweight cursor_sample_pos()
(position/visibility only, no shape extract) called from the pump.
2026-06-18 22:06:55 +03:00
lirent 9f1b41cf78 ci: run release job steps under bash
Default shell was sh (dash), which lacks `set -o pipefail`; pin shell: bash.
2026-06-17 18:04:30 +03:00
lirent 34a52659af ci: release windows exe to gitea release on v* tags
- .gitea/workflows/release.yml: mingw-w64 cross-compile in container, package exe+LICENSE, publish to the tag release via Gitea API
- LICENSE: MIT
- .gitignore: un-ignore .gitea/
2026-06-17 17:58:18 +03:00
9 changed files with 149 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
name: release
on:
push:
tags:
- 'v*'
jobs:
windows-exe:
runs-on: ubuntu-latest
container:
image: node:20-bookworm-slim
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install toolchain
run: |
apt-get update
apt-get install -y --no-install-recommends \
cmake make zip jq curl ca-certificates \
gcc-mingw-w64-x86-64
- name: Configure
run: >
cmake -S . -B build
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-mingw-w64.cmake
- name: Build
run: cmake --build build -j
- name: Package
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
mkdir -p dist/vgpu-streamer
cp build/vgpu-streamer.exe dist/vgpu-streamer/
[ -f LICENSE ] && cp LICENSE dist/vgpu-streamer/ || true
(cd dist && zip -r "vgpu-streamer-${TAG}-win64.zip" vgpu-streamer)
- name: Publish to release
env:
GITEA_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
SERVER: ${{ github.server_url }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
asset="vgpu-streamer-${TAG}-win64.zip"
api="${SERVER}/api/v1/repos/${REPO}"
auth="Authorization: token ${GITEA_TOKEN}"
# find the release for this tag, or create it
rid=$(curl -sSL -H "$auth" "${api}/releases/tags/${TAG}" | jq -r '.id // empty' || true)
if [ -z "$rid" ]; then
rid=$(curl -fsSL -X POST -H "$auth" -H "Content-Type: application/json" \
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\"}" \
"${api}/releases" | jq -r '.id')
fi
# drop a prior asset of the same name so re-runs are idempotent
curl -fsSL -H "$auth" "${api}/releases/${rid}/assets" \
| jq -r ".[] | select(.name==\"${asset}\") | .id" \
| while read -r aid; do
[ -n "$aid" ] && curl -fsSL -X DELETE -H "$auth" "${api}/releases/${rid}/assets/${aid}"
done
curl -fsSL -X POST -H "$auth" \
-F "attachment=@dist/${asset};type=application/zip" \
"${api}/releases/${rid}/assets?name=${asset}"
+1
View File
@@ -1,4 +1,5 @@
.*/ .*/
!.gitea/
cmake-*/ cmake-*/
compile* compile*
docs/plans/ docs/plans/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024-2026 Gregory Lirent <opensource@lirent.ru>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+15
View File
@@ -69,6 +69,18 @@ typedef struct {
uint32_t supported_formats; /* bitmask (1u<<VGPU_FMT_*) */ uint32_t supported_formats; /* bitmask (1u<<VGPU_FMT_*) */
uint32_t ctrl_ack; /* echo of control.ctrl_gen (even) applied */ uint32_t ctrl_ack; /* echo of control.ctrl_gen (even) applied */
uint32_t full_frame_ack; /* echo of control.full_frame_req honored */ uint32_t full_frame_ack; /* echo of control.full_frame_req honored */
/* --- cursor reporting (host-RO; position is sensor data, independent
* of control.draw_cursor / cursor compositing) --- */
uint32_t cursor_seq; /* @168: monotonic; bumps each cursor publish.
Host reads it last (acquire) to gate a
consistent {cursor_pos,cursor_visible}; lets the
host tell "cursor idle" from "producer stopped
reporting". */
uint32_t cursor_visible; /* @172: 1=cursor shown (CURSOR_SHOWING), 0=hidden */
uint64_t cursor_pos; /* @176: packed screen position, 8-aligned single
atomic MOV. low 32 bits = x, high 32 = y, each a
signed int32 (two's-complement; multi-monitor →
negatives). Pair never tears (one 64-bit store). */
} vgpu_producer_t; } vgpu_producer_t;
static_assert(alignof(vgpu_producer_t) == 64, "producer align"); static_assert(alignof(vgpu_producer_t) == 64, "producer align");
static_assert(sizeof(vgpu_producer_t) <= VGPU_PAGE, "producer fits page 0"); static_assert(sizeof(vgpu_producer_t) <= VGPU_PAGE, "producer fits page 0");
@@ -86,6 +98,9 @@ static_assert(offsetof(vgpu_producer_t, applied_fps) == 152, "producer.app
static_assert(offsetof(vgpu_producer_t, supported_formats) == 156, "producer.supported_formats"); static_assert(offsetof(vgpu_producer_t, supported_formats) == 156, "producer.supported_formats");
static_assert(offsetof(vgpu_producer_t, ctrl_ack) == 160, "producer.ctrl_ack"); static_assert(offsetof(vgpu_producer_t, ctrl_ack) == 160, "producer.ctrl_ack");
static_assert(offsetof(vgpu_producer_t, full_frame_ack) == 164, "producer.full_frame_ack"); static_assert(offsetof(vgpu_producer_t, full_frame_ack) == 164, "producer.full_frame_ack");
static_assert(offsetof(vgpu_producer_t, cursor_seq) == 168, "producer.cursor_seq");
static_assert(offsetof(vgpu_producer_t, cursor_visible) == 172, "producer.cursor_visible");
static_assert(offsetof(vgpu_producer_t, cursor_pos) == 176, "producer.cursor_pos");
/* ===== Control block (host-RW), own page, generation-guarded ===== */ /* ===== Control block (host-RW), own page, generation-guarded ===== */
typedef struct { typedef struct {
+7
View File
@@ -53,4 +53,11 @@ void vgpu_bump_run_epoch(const vgpu_region_view* rv);
void vgpu_tick_heartbeat(const vgpu_region_view* rv); void vgpu_tick_heartbeat(const vgpu_region_view* rv);
void vgpu_publish_full_frame_ack(const vgpu_region_view* rv, uint32_t req); void vgpu_publish_full_frame_ack(const vgpu_region_view* rv, uint32_t req);
/* Publish the on-screen cursor position (host-RO). Position is sensor data and is
* reported independent of control.draw_cursor (host may draw its own overlay even when the
* producer does not composite the cursor). x,y are screen coords (signed; multi-monitor may
* be negative); visible!=0 when the cursor is shown. Packs x|y into one 8-aligned 64-bit
* field (single atomic store) and bumps cursor_seq last. */
void vgpu_publish_cursor(const vgpu_region_view* rv, int32_t x, int32_t y, uint32_t visible);
#endif /* VGPU_STREAM_ENGINE_H */ #endif /* VGPU_STREAM_ENGINE_H */
+11
View File
@@ -116,3 +116,14 @@ void vgpu_tick_heartbeat(const vgpu_region_view* rv) {
void vgpu_publish_full_frame_ack(const vgpu_region_view* rv, uint32_t req) { void vgpu_publish_full_frame_ack(const vgpu_region_view* rv, uint32_t req) {
vgpu_store_release32(&rv->producer->full_frame_ack, req); vgpu_store_release32(&rv->producer->full_frame_ack, req);
} }
void vgpu_publish_cursor(const vgpu_region_view* rv, int32_t x, int32_t y, uint32_t visible) {
vgpu_producer_t* p = rv->producer;
/* pack: low 32 = x, high 32 = y (signed → two's-complement bits) */
uint64_t packed = ((uint64_t)(uint32_t)y << 32) | (uint64_t)(uint32_t)x;
/* 64-bit aligned single MOV is atomic on x86_64; barrier orders it (heartbeat pattern) */
p->cursor_pos = packed;
vgpu_store_release32(&p->cursor_visible, visible);
/* publish seq last: its release-store gates the pos/visible writes above for the host */
vgpu_store_release32(&p->cursor_seq, p->cursor_seq + 1u);
}
+9
View File
@@ -105,6 +105,15 @@ int cursor_sample(vgpu_ctx* ctx) {
return changed; return changed;
} }
void cursor_sample_pos(vgpu_ctx* ctx) {
vgpu_cursor_t* cur = &ctx->cursor;
CURSORINFO ci; ci.cbSize = sizeof ci;
if (!GetCursorInfo(&ci)) { cur->visible = 0; return; }
cur->visible = (ci.flags & CURSOR_SHOWING) != 0;
cur->x = ci.ptScreenPos.x;
cur->y = ci.ptScreenPos.y;
}
void cursor_draw(vgpu_ctx* ctx, uint8_t* dst, uint32_t W, uint32_t H) { void cursor_draw(vgpu_ctx* ctx, uint8_t* dst, uint32_t W, uint32_t H) {
vgpu_cursor_t* cur = &ctx->cursor; vgpu_cursor_t* cur = &ctx->cursor;
if (!cur->visible || cur->gw == 0) return; if (!cur->visible || cur->gw == 0) return;
+4
View File
@@ -10,6 +10,10 @@
* Returns 1 if anything changed since last sample, else 0. */ * Returns 1 if anything changed since last sample, else 0. */
int cursor_sample(vgpu_ctx* ctx); int cursor_sample(vgpu_ctx* ctx);
/* Sample only cursor position/visibility (no shape extract). Updates
* ctx->cursor.{visible,x,y}. Cheap (one GetCursorInfo); safe to call every pump tick. */
void cursor_sample_pos(vgpu_ctx* ctx);
/* Alpha/AND-XOR compose the sampled cursor onto a tight BGRA frame. */ /* Alpha/AND-XOR compose the sampled cursor onto a tight BGRA frame. */
void cursor_draw(vgpu_ctx* ctx, uint8_t* bgra, uint32_t width, uint32_t height); void cursor_draw(vgpu_ctx* ctx, uint8_t* bgra, uint32_t width, uint32_t height);
+6
View File
@@ -100,6 +100,12 @@ void vgpu_present_run(vgpu_ctx* ctx) {
last_beat = nowt; last_beat = nowt;
} }
/* --- cursor position: sensor data, reported every tick independent of
* draw_cursor / compositing (host may overlay it itself) --- */
cursor_sample_pos(ctx);
vgpu_publish_cursor(rv, (int32_t)ctx->cursor.x, (int32_t)ctx->cursor.y,
(uint32_t)(ctx->cursor.visible != 0));
/* --- reconcile control (gen-seqlock -> apply -> ack) --- */ /* --- reconcile control (gen-seqlock -> apply -> ack) --- */
vgpu_control_view cv; vgpu_control_view cv;
uint32_t desired = prev_state; uint32_t desired = prev_state;