Compare commits

...

9 Commits

Author SHA1 Message Date
92ba1adf29 docs: add discovery flow diagrams, document restart detection limitation
- Four Mermaid sequence diagrams: startup, steady-state keepalive, node
  loss/timeout, and node restart
- Explicitly document that the site_id-change restart heuristic does not work
  in practice (site_id is static config, not a runtime value)
- Describe what needs to change: a boot nonce (random u32 at startup)
- Add boot nonce as a deferred item in planning.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 04:39:52 +00:00
f3a6be0701 docs: update discovery behaviour — targeted unicast replies, not multicast
Document that immediate re-announcements go directly to the triggering peer
(unicast) rather than to the multicast group, and explain the two conditions
that trigger a reply: new peer and restarted peer (site_id change).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 04:35:28 +00:00
a5198e84d2 Discovery: reply unicast when responding to new/restarted peer
When we see a new peer or detect a restart (site_id change for known addr+port),
send the announcement directly to that host via unicast instead of broadcasting
to the multicast group. This avoids waking every other node on the subnet for a
reply that is only relevant to one machine.

The periodic multicast announcements continue unchanged for initial discovery.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 04:31:56 +00:00
780f45b46f Fix discovery re-announce: gate on new peer or restarted peer, not every packet
The original unconditional cond_signal (every received packet) caused a
multicast storm: each node instantly reflected every announcement back as its
own, creating a tight loop at wire speed.

The previous fix (gate on is_new only) broke the restart case: a peer that
restarts with the same addr+port is already in the table so is_new stays 0,
meaning we'd wait up to interval_ms before that peer learned about us.

Correct fix: also signal when site_id changes for a known addr+port entry,
which reliably indicates a restart. Steady-state keepalive packets (same
site_id) no longer trigger re-announcement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 04:31:14 +00:00
b4facf04be Fix multicast storm: only re-announce on new peer, not every received packet
The announce thread was being woken (triggering a multicast send) on every
received announcement packet, including ones from already-known peers. With
two or more nodes this created a feedback loop: each incoming packet triggered
an outbound multicast which triggered another incoming packet on the peer, and
so on at full CPU/network speed.

Gate the cond_signal on is_new so we only fast-announce when a genuinely new
peer is seen. The periodic interval-based announcement continues to handle
keepalives and reconnections for existing peers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 04:29:50 +00:00
8fa2f33bad Rename scale → scale_mode in protocol/struct layer; add control grouping future note
- `Proto_Display_Device_Info.scale` → `scale_mode`
- `Proto_Start_Display.scale` → `scale_mode`
- `PROTO_DISPLAY_CTRL_SCALE` → `PROTO_DISPLAY_CTRL_SCALE_MODE`
- `proto_write_start_display` param and all callers updated
- `on_display` callback param and all sites updated
- `Display_Slot.scale` → `scale_mode` in node
- Control name "Scale" → "Scale Mode"
- planning.md: add control grouping deferred decision

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 00:54:22 +00:00
7777292dfd planning: revert free pan/zoom note to original wording
scale is scale_mode, zoom_factor is a separate multiplier — they compose
rather than conflict.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 00:48:09 +00:00
87b9800e41 planning: clarify free mode replaces scale modes rather than composing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 00:47:41 +00:00
86f135792f planning: note free pan/zoom mode for display viewer
Current anchor system only handles fixed alignment; free mode needed
for arbitrary pan offset + zoom level, e.g. microscope inspection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 00:46:52 +00:00
8 changed files with 138 additions and 50 deletions

View File

@@ -163,7 +163,7 @@ static void on_standalone(
(*idx)++; (*idx)++;
} }
static const char *scale_name(uint8_t s) static const char *scale_mode_name(uint8_t s)
{ {
switch (s) { switch (s) {
case 0: return "stretch"; case 0: return "stretch";
@@ -179,13 +179,13 @@ static void on_display(
uint16_t stream_id, uint16_t stream_id,
int16_t win_x, int16_t win_y, int16_t win_x, int16_t win_y,
uint16_t win_w, uint16_t win_h, uint16_t win_w, uint16_t win_h,
uint8_t scale, uint8_t anchor, uint8_t scale_mode, uint8_t anchor,
void *ud) void *ud)
{ {
(void)ud; (void)ud;
printf(" [%u] display stream=%u pos=%d,%d size=%ux%u scale=%s anchor=%s\n", printf(" [%u] display stream=%u pos=%d,%d size=%ux%u scale_mode=%s anchor=%s\n",
device_id, stream_id, win_x, win_y, win_w, win_h, device_id, stream_id, win_x, win_y, win_w, win_h,
scale_name(scale), scale_mode_name(scale_mode),
anchor == 0 ? "center" : "topleft"); anchor == 0 ? "center" : "topleft");
} }

View File

@@ -1,6 +1,6 @@
# discovery_cli # discovery_cli
A development tool for testing the UDP multicast discovery layer. Announces a node on the local network and prints peers as they appear and disappear. A development tool for testing the discovery layer. Announces a node on the local network via UDP multicast and prints peers as they appear and disappear.
--- ---

View File

@@ -43,11 +43,81 @@ A node may set multiple bits — a relay that also archives sets both `RELAY` an
### Behaviour ### Behaviour
- Nodes send announcements periodically (e.g. every 5 s) and immediately on startup - Nodes send announcements periodically (default every 5 s) and immediately on startup via multicast
- No daemon — the node process itself sends and listens; no background service required - No daemon — the node process itself sends and listens; no background service required
- On receiving an announcement, the control plane records the peer (address, port, name, function) and can initiate a transport connection if needed - On receiving an announcement the node records the peer (address, port, name, capabilities) and can initiate a transport connection if needed
- A node going silent for a configured number of announcement intervals is considered offline - A peer that goes silent for `timeout_intervals × interval_ms` is considered offline and removed from the peer table
- Announcements are informational only — the hub validates identity at connection time - Announcements are informational only — identity is validated at TCP connection time
#### Startup — new node joins the network
```mermaid
sequenceDiagram
participant N as New Node
participant MC as Multicast group
participant A as Node A
participant B as Node B
N->>MC: announce (multicast)
MC-->>A: receives announce
MC-->>B: receives announce
A->>N: announce (unicast reply)
B->>N: announce (unicast reply)
Note over N,B: All parties now know each other.<br/>Subsequent keepalives are multicast only.
```
Each node that hears a new peer sends a **unicast reply** directly to that peer. This allows the new node to populate its peer table within one round-trip rather than waiting up to `interval_ms` for other nodes' next scheduled broadcast.
#### Steady-state keepalive
```mermaid
sequenceDiagram
participant A as Node A
participant MC as Multicast group
participant B as Node B
participant C as Node C
loop every interval_ms
A->>MC: announce (multicast)
MC-->>B: receives — updates last_seen_ms, no reply
MC-->>C: receives — updates last_seen_ms, no reply
end
```
Known peers update their `last_seen_ms` timestamp and do nothing else. No reply is sent, so there is no amplification.
#### Node loss — timeout
```mermaid
sequenceDiagram
participant A as Node A
participant B as Node B (offline)
Note over B: Node B stops sending
loop timeout_intervals × interval_ms elapses
A->>A: check_timeouts() — not yet expired
end
A->>A: check_timeouts() — expired, remove B
A->>A: on_peer_lost(B) callback
```
#### Node restart — known limitation
The current implementation attempts to detect a restart by checking whether `site_id` changed for a known `(addr, port)` entry. In practice this **does not work**: `site_id` is a static configuration value and will be the same before and after a restart. A restarted node will therefore simply be treated as a continuing keepalive and will not receive an immediate unicast reply — it will have to wait up to `interval_ms` for the next scheduled multicast broadcast from its peers.
```mermaid
sequenceDiagram
participant R as Restarted Node
participant MC as Multicast group
participant A as Node A
Note over R: Node restarts — same addr, port, site_id
R->>MC: announce (multicast)
MC-->>A: receives — site_id unchanged, treated as keepalive
Note over A: No unicast reply sent. R waits up to interval_ms<br/>to learn about A via A's next scheduled multicast.
```
**What needs to change:** a **boot nonce** (random `u32` generated at startup, not configured) should be added to the announcement payload. A change in boot nonce for a known peer unambiguously signals a restart and triggers an immediate unicast reply. This requires a wire format version bump and updates to the peer table struct, announcement builder, and receive logic.
### No Avahi/Bonjour Dependency ### No Avahi/Bonjour Dependency

View File

@@ -157,7 +157,7 @@ struct Proto_Display_Device_Info {
uint16_t stream_id; uint16_t stream_id;
int16_t win_x, win_y; int16_t win_x, win_y;
uint16_t win_w, win_h; uint16_t win_w, win_h;
uint8_t scale; uint8_t scale_mode;
uint8_t anchor; uint8_t anchor;
}; };
@@ -166,7 +166,7 @@ struct Proto_Display_Device_Info {
* SET_CONTROL for display device indices returned by ENUM_DEVICES. * SET_CONTROL for display device indices returned by ENUM_DEVICES.
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
#define PROTO_DISPLAY_CTRL_SCALE 0x00D00001u /* int 0-3: stretch/fit/fill/1:1 */ #define PROTO_DISPLAY_CTRL_SCALE_MODE 0x00D00001u /* int 0-3: stretch/fit/fill/1:1 */
#define PROTO_DISPLAY_CTRL_ANCHOR 0x00D00002u /* int 0-1: center/topleft */ #define PROTO_DISPLAY_CTRL_ANCHOR 0x00D00002u /* int 0-1: center/topleft */
#define PROTO_DISPLAY_CTRL_NO_SIGNAL_FPS 0x00D00003u /* int 1-60: no-signal animation fps */ #define PROTO_DISPLAY_CTRL_NO_SIGNAL_FPS 0x00D00003u /* int 1-60: no-signal animation fps */
@@ -262,7 +262,7 @@ struct Proto_Stop_Ingest {
* display incoming VIDEO_FRAME messages for the given stream_id. * display incoming VIDEO_FRAME messages for the given stream_id.
* win_x/win_y are screen-space window position (signed: multi-monitor). * win_x/win_y are screen-space window position (signed: multi-monitor).
* win_w/win_h of 0 mean use a default size. * win_w/win_h of 0 mean use a default size.
* scale: 0=stretch 1=fit 2=fill 3=1:1 (PROTO_DISPLAY_SCALE_*) * scale_mode: 0=stretch 1=fit 2=fill 3=1:1 (PROTO_DISPLAY_SCALE_*)
* anchor: 0=center 1=topleft (PROTO_DISPLAY_ANCHOR_*) * anchor: 0=center 1=topleft (PROTO_DISPLAY_ANCHOR_*)
*/ */
struct Proto_Start_Display { struct Proto_Start_Display {
@@ -272,7 +272,7 @@ struct Proto_Start_Display {
int16_t win_y; int16_t win_y;
uint16_t win_w; uint16_t win_w;
uint16_t win_h; uint16_t win_h;
uint8_t scale; uint8_t scale_mode;
uint8_t anchor; uint8_t anchor;
uint8_t no_signal_fps; /* 0 = default (15); no-signal animation frame rate */ uint8_t no_signal_fps; /* 0 = default (15); no-signal animation frame rate */
/* 1 byte reserved */ /* 1 byte reserved */
@@ -365,7 +365,7 @@ struct App_Error proto_write_stop_ingest(struct Transport_Conn *conn,
struct App_Error proto_write_start_display(struct Transport_Conn *conn, struct App_Error proto_write_start_display(struct Transport_Conn *conn,
uint16_t request_id, uint16_t stream_id, uint16_t request_id, uint16_t stream_id,
int16_t win_x, int16_t win_y, uint16_t win_w, uint16_t win_h, int16_t win_x, int16_t win_y, uint16_t win_w, uint16_t win_h,
uint8_t scale, uint8_t anchor, uint8_t no_signal_fps); uint8_t scale_mode, uint8_t anchor, uint8_t no_signal_fps);
/* CONTROL_REQUEST: STOP_DISPLAY */ /* CONTROL_REQUEST: STOP_DISPLAY */
struct App_Error proto_write_stop_display(struct Transport_Conn *conn, struct App_Error proto_write_stop_display(struct Transport_Conn *conn,
@@ -510,7 +510,7 @@ struct App_Error proto_read_enum_devices_response(
uint16_t stream_id, uint16_t stream_id,
int16_t win_x, int16_t win_y, int16_t win_x, int16_t win_y,
uint16_t win_w, uint16_t win_h, uint16_t win_w, uint16_t win_h,
uint8_t scale, uint8_t anchor, uint8_t scale_mode, uint8_t anchor,
void *userdata), void *userdata),
void *userdata); void *userdata);

View File

@@ -132,6 +132,9 @@ These are open questions tracked in `architecture.md` that do not need to be res
- Hard vs soft byte budget limits - Hard vs soft byte budget limits
- Cooperative capture release: if a capture source has no live downstream targets for a configurable time window, stop capture and release the device. Intended as a resource-conservation policy rather than an immediate reaction to disconnect events. Requires the node to track downstream liveness (e.g. last successful send timestamp per output) and implement a reaper timer. - Cooperative capture release: if a capture source has no live downstream targets for a configurable time window, stop capture and release the device. Intended as a resource-conservation policy rather than an immediate reaction to disconnect events. Requires the node to track downstream liveness (e.g. last successful send timestamp per output) and implement a reaper timer.
- Unified device model: active display windows should be registered as devices alongside V4L2 cameras, using the same ENUM_DEVICES / ENUM_CONTROLS / GET_CONTROL / SET_CONTROL protocol. START_DISPLAY would return a device_id for the opened window; controls (scale, anchor, position, size, zoom, pan) are then addressable as (device_id, control_id) pairs like any other device. Requires a device_type field in ENUM_DEVICES responses so controllers can distinguish V4L2 devices from display windows. Future device types: codec processes, screen grab sources. This extends naturally to shader-based post-processing and other viewer state as controls. - Unified device model: active display windows should be registered as devices alongside V4L2 cameras, using the same ENUM_DEVICES / ENUM_CONTROLS / GET_CONTROL / SET_CONTROL protocol. START_DISPLAY would return a device_id for the opened window; controls (scale, anchor, position, size, zoom, pan) are then addressable as (device_id, control_id) pairs like any other device. Requires a device_type field in ENUM_DEVICES responses so controllers can distinguish V4L2 devices from display windows. Future device types: codec processes, screen grab sources. This extends naturally to shader-based post-processing and other viewer state as controls.
- Display viewer free pan/zoom mode: the current anchor system (center/topleft) only covers fixed alignment. A "free" mode should allow the controller (or the user via mouse/keyboard in the window) to set arbitrary pan offset and zoom level independently of the scale mode. The xorg viewer would need pan_x/pan_y (normalised or pixel offsets) and zoom_factor controls alongside the existing scale/anchor. This is a prerequisite for use cases like microscope inspection where the user needs to freely navigate a high-resolution source.
- controller_cli is a temporary dev tool; the long-term replacement is a dedicated `controller` binary outside `dev/cli/` that maintains simultaneous connections to all discovered nodes (not switching between them). Commands address a specific node by peer index. This mirrors the web UI's model of administering the whole network rather than one node at a time. The `connect` / active-connection model in the current controller_cli is an interim design choice that should not be carried forward. - controller_cli is a temporary dev tool; the long-term replacement is a dedicated `controller` binary outside `dev/cli/` that maintains simultaneous connections to all discovered nodes (not switching between them). Commands address a specific node by peer index. This mirrors the web UI's model of administering the whole network rather than one node at a time. The `connect` / active-connection model in the current controller_cli is an interim design choice that should not be carried forward.
- start-ingest peer addressing: the `dest_host` + `dest_port` in START_INGEST is awkward to type manually and requires the caller to know the target's TCP port. Should accept a peer ID (index from the discovered peer table on the node) so the node can resolve the address itself. Requires the node to run discovery and expose its peer table. - start-ingest peer addressing: the `dest_host` + `dest_port` in START_INGEST is awkward to type manually and requires the caller to know the target's TCP port. Should accept a peer ID (index from the discovered peer table on the node) so the node can resolve the address itself. Requires the node to run discovery and expose its peer table.
- Connection multiplexing: currently each ingest stream opens its own outbound TCP connection to the destination. Multiple streams between the same two peers should share one connection, with stream_id used to demultiplex frames. This is the priority/encapsulation scheme described in the architecture — high-priority and low-latency frames from different streams travel over the same socket rather than competing across separate sockets. - Connection multiplexing: currently each ingest stream opens its own outbound TCP connection to the destination. Multiple streams between the same two peers should share one connection, with stream_id used to demultiplex frames. This is the priority/encapsulation scheme described in the architecture — high-priority and low-latency frames from different streams travel over the same socket rather than competing across separate sockets.
- Discovery boot nonce: the announcement payload needs a `boot_nonce` field (random u32 generated at startup, not configured). The current restart detection uses `site_id` change as a proxy, but `site_id` is static config and does not change on restart, so restarts are not detected and the restarted node waits up to `interval_ms` for peers to reply. Adding a boot nonce gives a reliable restart signal: a nonce change for a known (addr, port) entry triggers an immediate unicast reply. Requires a wire format version bump, peer table struct update, and changes to the announcement builder and receive logic.
- Control grouping: controls should be organizable into named groups for both display organisation (collapsible sections in a UI) and protocol semantics (enumerate controls within a group, set a group of related controls atomically). Relevant for display devices where scale_mode, anchor, position, and size are logically related, and for cameras where white balance, exposure, and gain belong together. The current flat list of (control_id, name, type, value) tuples does not capture this.

View File

@@ -76,19 +76,14 @@ static int find_slot(struct Discovery *d) {
/* -- send ------------------------------------------------------------------ */ /* -- send ------------------------------------------------------------------ */
static void send_announcement(struct Discovery *d) { static size_t build_announcement(struct Discovery *d, uint8_t *buf) {
size_t name_len = strlen(d->config.name); size_t name_len = strlen(d->config.name);
if (name_len > DISCOVERY_MAX_NAME_LEN) { name_len = DISCOVERY_MAX_NAME_LEN; } if (name_len > DISCOVERY_MAX_NAME_LEN) { name_len = DISCOVERY_MAX_NAME_LEN; }
uint32_t payload_len = (uint32_t)(ANN_FIXED_SIZE + name_len); uint32_t payload_len = (uint32_t)(ANN_FIXED_SIZE + name_len);
size_t total = TRANSPORT_FRAME_HEADER_SIZE + payload_len; put_u16(buf, 0, 0x0010);
uint8_t buf[TRANSPORT_FRAME_HEADER_SIZE + ANN_FIXED_SIZE + DISCOVERY_MAX_NAME_LEN];
/* frame header */
put_u16(buf, 0, 0x0010); /* message_type: DISCOVERY_ANNOUNCE */
put_u32(buf, 2, payload_len); put_u32(buf, 2, payload_len);
/* announcement payload */
uint8_t *p = buf + TRANSPORT_FRAME_HEADER_SIZE; uint8_t *p = buf + TRANSPORT_FRAME_HEADER_SIZE;
put_u8 (p, ANN_PROTOCOL_VERSION, DISCOVERY_PROTOCOL_VERSION); put_u8 (p, ANN_PROTOCOL_VERSION, DISCOVERY_PROTOCOL_VERSION);
put_u16(p, ANN_SITE_ID, d->config.site_id); put_u16(p, ANN_SITE_ID, d->config.site_id);
@@ -97,10 +92,26 @@ static void send_announcement(struct Discovery *d) {
put_u8 (p, ANN_NAME_LEN, (uint8_t)name_len); put_u8 (p, ANN_NAME_LEN, (uint8_t)name_len);
memcpy(p + ANN_NAME, d->config.name, name_len); memcpy(p + ANN_NAME, d->config.name, name_len);
return TRANSPORT_FRAME_HEADER_SIZE + payload_len;
}
static void send_announcement(struct Discovery *d) {
uint8_t buf[TRANSPORT_FRAME_HEADER_SIZE + ANN_FIXED_SIZE + DISCOVERY_MAX_NAME_LEN];
size_t total = build_announcement(d, buf);
sendto(d->sock, buf, total, 0, sendto(d->sock, buf, total, 0,
(struct sockaddr *)&d->mcast_addr, sizeof(d->mcast_addr)); (struct sockaddr *)&d->mcast_addr, sizeof(d->mcast_addr));
} }
static void send_announcement_unicast(struct Discovery *d, uint32_t addr) {
uint8_t buf[TRANSPORT_FRAME_HEADER_SIZE + ANN_FIXED_SIZE + DISCOVERY_MAX_NAME_LEN];
size_t total = build_announcement(d, buf);
struct sockaddr_in dest = {0};
dest.sin_family = AF_INET;
dest.sin_port = htons(DISCOVERY_PORT);
dest.sin_addr.s_addr = addr;
sendto(d->sock, buf, total, 0, (struct sockaddr *)&dest, sizeof(dest));
}
/* -- timeout check --------------------------------------------------------- */ /* -- timeout check --------------------------------------------------------- */
static void check_timeouts(struct Discovery *d) { static void check_timeouts(struct Discovery *d) {
@@ -204,13 +215,16 @@ static void *receive_thread_fn(void *arg) {
uint32_t addr = src.sin_addr.s_addr; uint32_t addr = src.sin_addr.s_addr;
uint64_t ts = now_ms(); uint64_t ts = now_ms();
int is_new = 0; int is_new = 0;
int reannounce = 0;
struct Discovery_Peer peer_copy; struct Discovery_Peer peer_copy;
pthread_mutex_lock(&d->peers_mutex); pthread_mutex_lock(&d->peers_mutex);
int idx = find_peer(d, addr, tcp_port); int idx = find_peer(d, addr, tcp_port);
if (idx >= 0) { if (idx >= 0) {
/* detect restart: same addr+port but site_id changed */
if (d->peers[idx].info.site_id != site_id) { reannounce = 1; }
d->peers[idx].last_seen_ms = ts; d->peers[idx].last_seen_ms = ts;
d->peers[idx].info.site_id = site_id; d->peers[idx].info.site_id = site_id;
d->peers[idx].info.tcp_port = tcp_port; d->peers[idx].info.tcp_port = tcp_port;
@@ -233,11 +247,12 @@ static void *receive_thread_fn(void *arg) {
pthread_mutex_unlock(&d->peers_mutex); pthread_mutex_unlock(&d->peers_mutex);
/* respond to every announcement — the sender may be a fresh instance if (is_new || reannounce) {
* that doesn't know about us yet even if we already have it in our table */ /* new peer, or peer restarted (site_id changed) — reply directly
pthread_mutex_lock(&d->announce_mutex); * to that host so it learns about us without waiting up to interval_ms.
pthread_cond_signal(&d->announce_cond); * Use unicast rather than multicast to avoid disturbing other nodes. */
pthread_mutex_unlock(&d->announce_mutex); send_announcement_unicast(d, addr);
}
if (is_new && d->config.on_peer_found) { if (is_new && d->config.on_peer_found) {
d->config.on_peer_found(&peer_copy, d->config.userdata); d->config.on_peer_found(&peer_copy, d->config.userdata);

View File

@@ -420,7 +420,7 @@ struct App_Error proto_write_enum_devices_response(struct Transport_Conn *conn,
e = wbuf_i16(&b, d->win_y); if (!APP_IS_OK(e)) { goto fail; } e = wbuf_i16(&b, d->win_y); if (!APP_IS_OK(e)) { goto fail; }
e = wbuf_u16(&b, d->win_w); if (!APP_IS_OK(e)) { goto fail; } e = wbuf_u16(&b, d->win_w); if (!APP_IS_OK(e)) { goto fail; }
e = wbuf_u16(&b, d->win_h); if (!APP_IS_OK(e)) { goto fail; } e = wbuf_u16(&b, d->win_h); if (!APP_IS_OK(e)) { goto fail; }
e = wbuf_u8 (&b, d->scale); if (!APP_IS_OK(e)) { goto fail; } e = wbuf_u8 (&b, d->scale_mode); if (!APP_IS_OK(e)) { goto fail; }
e = wbuf_u8 (&b, d->anchor); if (!APP_IS_OK(e)) { goto fail; } e = wbuf_u8 (&b, d->anchor); if (!APP_IS_OK(e)) { goto fail; }
} }
@@ -624,11 +624,11 @@ struct App_Error proto_read_stop_ingest(
} }
/* START_DISPLAY: request_id(2) cmd(2) stream_id(2) win_x(2) win_y(2) /* START_DISPLAY: request_id(2) cmd(2) stream_id(2) win_x(2) win_y(2)
* win_w(2) win_h(2) scale(1) anchor(1) no_signal_fps(1) reserved(1) = 18 bytes */ * win_w(2) win_h(2) scale_mode(1) anchor(1) no_signal_fps(1) reserved(1) = 18 bytes */
struct App_Error proto_write_start_display(struct Transport_Conn *conn, struct App_Error proto_write_start_display(struct Transport_Conn *conn,
uint16_t request_id, uint16_t stream_id, uint16_t request_id, uint16_t stream_id,
int16_t win_x, int16_t win_y, uint16_t win_w, uint16_t win_h, int16_t win_x, int16_t win_y, uint16_t win_w, uint16_t win_h,
uint8_t scale, uint8_t anchor, uint8_t no_signal_fps) uint8_t scale_mode, uint8_t anchor, uint8_t no_signal_fps)
{ {
uint8_t buf[18]; uint8_t buf[18];
uint32_t o = 0; uint32_t o = 0;
@@ -639,7 +639,7 @@ struct App_Error proto_write_start_display(struct Transport_Conn *conn,
put_i16(buf, o, win_y); o += 2; put_i16(buf, o, win_y); o += 2;
put_u16(buf, o, win_w); o += 2; put_u16(buf, o, win_w); o += 2;
put_u16(buf, o, win_h); o += 2; put_u16(buf, o, win_h); o += 2;
put_u8 (buf, o, scale); o += 1; put_u8 (buf, o, scale_mode); o += 1;
put_u8 (buf, o, anchor); o += 1; put_u8 (buf, o, anchor); o += 1;
put_u8 (buf, o, no_signal_fps); o += 1; put_u8 (buf, o, no_signal_fps); o += 1;
put_u8 (buf, o, 0); o += 1; /* reserved */ put_u8 (buf, o, 0); o += 1; /* reserved */
@@ -669,7 +669,7 @@ struct App_Error proto_read_start_display(
out->win_y = get_i16(payload, 8); out->win_y = get_i16(payload, 8);
out->win_w = get_u16(payload, 10); out->win_w = get_u16(payload, 10);
out->win_h = get_u16(payload, 12); out->win_h = get_u16(payload, 12);
out->scale = get_u8 (payload, 14); out->scale_mode = get_u8 (payload, 14);
out->anchor = get_u8 (payload, 15); out->anchor = get_u8 (payload, 15);
out->no_signal_fps = length >= 18 ? get_u8(payload, 16) : 0; out->no_signal_fps = length >= 18 ? get_u8(payload, 16) : 0;
return APP_OK; return APP_OK;
@@ -731,7 +731,7 @@ struct App_Error proto_read_enum_devices_response(
uint16_t stream_id, uint16_t stream_id,
int16_t win_x, int16_t win_y, int16_t win_x, int16_t win_y,
uint16_t win_w, uint16_t win_h, uint16_t win_w, uint16_t win_h,
uint8_t scale, uint8_t anchor, uint8_t scale_mode, uint8_t anchor,
void *userdata), void *userdata),
void *userdata) void *userdata)
{ {
@@ -798,12 +798,12 @@ struct App_Error proto_read_enum_devices_response(
int16_t win_y = (int16_t)cur_u16(&c); int16_t win_y = (int16_t)cur_u16(&c);
uint16_t win_w = cur_u16(&c); uint16_t win_w = cur_u16(&c);
uint16_t win_h = cur_u16(&c); uint16_t win_h = cur_u16(&c);
uint8_t scale = cur_u8(&c); uint8_t scale_mode = cur_u8(&c);
uint8_t anchor = cur_u8(&c); uint8_t anchor = cur_u8(&c);
CUR_CHECK(c); CUR_CHECK(c);
if (on_display) { if (on_display) {
on_display(device_id, stream_id, win_x, win_y, on_display(device_id, stream_id, win_x, win_y,
win_w, win_h, scale, anchor, userdata); win_w, win_h, scale_mode, anchor, userdata);
} }
} }
} }

View File

@@ -178,7 +178,7 @@ struct Display_Slot {
/* Config — written by handle_start_display before setting wanted */ /* Config — written by handle_start_display before setting wanted */
int win_x, win_y; int win_x, win_y;
int win_w, win_h; int win_w, win_h;
Xorg_Scale scale; Xorg_Scale scale_mode;
Xorg_Anchor anchor; Xorg_Anchor anchor;
/* Pending frame — deposited by transport thread, consumed by main */ /* Pending frame — deposited by transport thread, consumed by main */
@@ -485,7 +485,7 @@ static void display_loop_tick(struct Node *node)
Xorg_Viewer *v = xorg_viewer_open( Xorg_Viewer *v = xorg_viewer_open(
d->win_x, d->win_y, d->win_w, d->win_h, title); d->win_x, d->win_y, d->win_w, d->win_h, title);
if (v) { if (v) {
xorg_viewer_set_scale(v, d->scale); xorg_viewer_set_scale(v, d->scale_mode);
xorg_viewer_set_anchor(v, d->anchor); xorg_viewer_set_anchor(v, d->anchor);
d->viewer = v; d->viewer = v;
pthread_mutex_lock(&d->mutex); pthread_mutex_lock(&d->mutex);
@@ -512,7 +512,7 @@ static void display_loop_tick(struct Node *node)
/* Sync scale/anchor — may be updated live via SET_CONTROL */ /* Sync scale/anchor — may be updated live via SET_CONTROL */
pthread_mutex_lock(&d->mutex); pthread_mutex_lock(&d->mutex);
Xorg_Scale cur_scale = d->scale; Xorg_Scale cur_scale = d->scale_mode;
Xorg_Anchor cur_anchor = d->anchor; Xorg_Anchor cur_anchor = d->anchor;
pthread_mutex_unlock(&d->mutex); pthread_mutex_unlock(&d->mutex);
xorg_viewer_set_scale(d->viewer, cur_scale); xorg_viewer_set_scale(d->viewer, cur_scale);
@@ -889,7 +889,7 @@ static void handle_enum_devices(struct Node *node,
.win_y = (int16_t)d->win_y, .win_y = (int16_t)d->win_y,
.win_w = (uint16_t)d->win_w, .win_w = (uint16_t)d->win_w,
.win_h = (uint16_t)d->win_h, .win_h = (uint16_t)d->win_h,
.scale = (uint8_t)d->scale, .scale_mode = (uint8_t)d->scale_mode,
.anchor = (uint8_t)d->anchor, .anchor = (uint8_t)d->anchor,
}; };
pthread_mutex_unlock(&d->mutex); pthread_mutex_unlock(&d->mutex);
@@ -923,15 +923,15 @@ static void handle_enum_controls(struct Node *node,
return; return;
} }
pthread_mutex_lock(&disp->mutex); pthread_mutex_lock(&disp->mutex);
int scale = (int)disp->scale; int scale_mode = (int)disp->scale_mode;
int anchor = (int)disp->anchor; int anchor = (int)disp->anchor;
int no_signal_fps = disp->no_signal_fps > 0 ? disp->no_signal_fps : 15; int no_signal_fps = disp->no_signal_fps > 0 ? disp->no_signal_fps : 15;
pthread_mutex_unlock(&disp->mutex); pthread_mutex_unlock(&disp->mutex);
struct Proto_Control_Info ctrls[] = { struct Proto_Control_Info ctrls[] = {
{ .id = PROTO_DISPLAY_CTRL_SCALE, { .id = PROTO_DISPLAY_CTRL_SCALE_MODE,
.type = 1, .name = "Scale", .type = 1, .name = "Scale Mode",
.min = 0, .max = 3, .step = 1, .default_val = 1, .min = 0, .max = 3, .step = 1, .default_val = 1,
.current_val = scale }, .current_val = scale_mode },
{ .id = PROTO_DISPLAY_CTRL_ANCHOR, { .id = PROTO_DISPLAY_CTRL_ANCHOR,
.type = 1, .name = "Anchor", .type = 1, .name = "Anchor",
.min = 0, .max = 1, .step = 1, .default_val = 0, .min = 0, .max = 1, .step = 1, .default_val = 0,
@@ -981,7 +981,7 @@ static void handle_get_control(struct Node *node,
int32_t value = 0; int32_t value = 0;
int found = 1; int found = 1;
switch (req.control_id) { switch (req.control_id) {
case PROTO_DISPLAY_CTRL_SCALE: value = (int32_t)disp->scale; break; case PROTO_DISPLAY_CTRL_SCALE_MODE: value = (int32_t)disp->scale_mode; break;
case PROTO_DISPLAY_CTRL_ANCHOR: value = (int32_t)disp->anchor; break; case PROTO_DISPLAY_CTRL_ANCHOR: value = (int32_t)disp->anchor; break;
case PROTO_DISPLAY_CTRL_NO_SIGNAL_FPS: value = disp->no_signal_fps > 0 ? disp->no_signal_fps : 15; break; case PROTO_DISPLAY_CTRL_NO_SIGNAL_FPS: value = disp->no_signal_fps > 0 ? disp->no_signal_fps : 15; break;
default: found = 0; break; default: found = 0; break;
@@ -1032,9 +1032,9 @@ static void handle_set_control(struct Node *node,
pthread_mutex_lock(&disp->mutex); pthread_mutex_lock(&disp->mutex);
int found = 1; int found = 1;
switch (req.control_id) { switch (req.control_id) {
case PROTO_DISPLAY_CTRL_SCALE: case PROTO_DISPLAY_CTRL_SCALE_MODE:
if (req.value >= 0 && req.value <= 3) { if (req.value >= 0 && req.value <= 3) {
disp->scale = (Xorg_Scale)req.value; disp->scale_mode = (Xorg_Scale)req.value;
} }
break; break;
case PROTO_DISPLAY_CTRL_ANCHOR: case PROTO_DISPLAY_CTRL_ANCHOR:
@@ -1250,7 +1250,7 @@ static void handle_start_display(struct Node *node,
d->win_y = (int)req.win_y; d->win_y = (int)req.win_y;
d->win_w = req.win_w > 0 ? (int)req.win_w : 1280; d->win_w = req.win_w > 0 ? (int)req.win_w : 1280;
d->win_h = req.win_h > 0 ? (int)req.win_h : 720; d->win_h = req.win_h > 0 ? (int)req.win_h : 720;
d->scale = proto_scale_to_xorg(req.scale); d->scale_mode = proto_scale_to_xorg(req.scale_mode);
d->anchor = proto_anchor_to_xorg(req.anchor); d->anchor = proto_anchor_to_xorg(req.anchor);
d->no_signal_fps = req.no_signal_fps > 0 ? (int)req.no_signal_fps : 15; d->no_signal_fps = req.no_signal_fps > 0 ? (int)req.no_signal_fps : 15;
d->wanted_state = DISP_OPEN; /* reconciled by display_loop_tick */ d->wanted_state = DISP_OPEN; /* reconciled by display_loop_tick */