Skip to main content

mx_remote_ffi/
control.rs

1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! The control surface: what a caller can ask a device to do.
5//!
6//! Every call here returns `MXR_OK` only when a frame left the socket. There
7//! is nothing further to wait for and nothing to acknowledge: a device answers
8//! a command by reporting its new state a moment later, through the callbacks,
9//! so a caller that needs confirmation waits for the event rather than for the
10//! return.
11//!
12//! A device that speaks a protocol older than a command requires is refused
13//! with `MXR_ERR_PROTOCOL_TOO_OLD` and nothing is sent, because such a device
14//! discards the frame without answering and a send would report a success that
15//! changed nothing.
16
17use std::ffi::c_char;
18
19use mx_remote::{
20    DeviceUid, EdidProfile, MultiviewerAspectRatio, MultiviewerEdidTemplate, MultiviewerHdcpMode,
21    MultiviewerItcMode, MultiviewerOutputMode, MultiviewerPipPosition, MultiviewerPipSize,
22    MultiviewerSource, MultiviewerViewMode, RcAction, RcKey, V2ipAudioFormat, V2ipColourSpace,
23    V2ipDeviceSetting, V2ipOutputMode, V2ipRoute, V2ipRouteTarget, VideoWallWindow,
24};
25
26use crate::abi::{
27    fail, from_control, mxr_bay_uid_t, mxr_result_t, mxr_tribool_t, mxr_uid_t, opt_str, req_str,
28};
29use crate::info::mxr_amp_zone_settings_t;
30use crate::remote::{mxr_remote_t, with};
31
32/// A stream's sample rate and channel count.
33#[repr(C)]
34#[derive(Clone, Copy)]
35pub struct mxr_audio_format_t {
36    /// Sample rate in Hz.
37    pub sample_rate: u32,
38    /// Channel count.
39    pub channels: u8,
40}
41
42impl From<mxr_audio_format_t> for V2ipAudioFormat {
43    fn from(f: mxr_audio_format_t) -> Self {
44        Self {
45            sample_rate: f.sample_rate,
46            channels: f.channels,
47        }
48    }
49}
50
51// ---- routing ----
52
53/// Routes a V2IP sink's video to the stream a source port advertises.
54///
55/// # Safety
56///
57/// `remote` is null or a live handle from `mxr_remote_new()`.
58#[no_mangle]
59pub unsafe extern "C" fn mxr_select_video_source(
60    remote: *const mxr_remote_t,
61    sink: mxr_bay_uid_t,
62    source_port: u16,
63) -> mxr_result_t {
64    // SAFETY: the caller guarantees a live handle or null.
65    let handle = unsafe { remote.as_ref() };
66    with(handle, |r| {
67        from_control(r.remote.select_video_source(sink.into(), source_port))
68    })
69}
70
71/// Routes a V2IP sink's audio to the stream a source port advertises,
72/// leaving its video where it is.
73///
74/// # Safety
75///
76/// `remote` is null or a live handle from `mxr_remote_new()`.
77#[no_mangle]
78pub unsafe extern "C" fn mxr_select_audio_source(
79    remote: *const mxr_remote_t,
80    sink: mxr_bay_uid_t,
81    source_port: u16,
82) -> mxr_result_t {
83    // SAFETY: the caller guarantees a live handle or null.
84    let handle = unsafe { remote.as_ref() };
85    with(handle, |r| {
86        from_control(r.remote.select_audio_source(sink.into(), source_port))
87    })
88}
89
90/// Routes a V2IP sink's video to the source bay with this user-assigned name.
91///
92/// # Safety
93///
94/// `remote` is null or a live handle, and `name` is a NUL-terminated string.
95#[no_mangle]
96pub unsafe extern "C" fn mxr_select_video_source_by_name(
97    remote: *const mxr_remote_t,
98    sink: mxr_bay_uid_t,
99    name: *const c_char,
100) -> mxr_result_t {
101    // SAFETY: the caller guarantees a live handle or null.
102    let handle = unsafe { remote.as_ref() };
103    with(handle, |r| {
104        // SAFETY: the caller guarantees a NUL-terminated string.
105        match unsafe { req_str(name) } {
106            Ok(name) => from_control(r.remote.select_video_source_by_name(sink.into(), name)),
107            Err(code) => code,
108        }
109    })
110}
111
112/// Routes a V2IP sink's audio to the source bay with this user-assigned name.
113///
114/// `format` may be null to leave the sink's audio format alone.
115///
116/// # Safety
117///
118/// `remote` is null or a live handle, `name` is a NUL-terminated string, and
119/// `format` is null or points at an initialised [`mxr_audio_format_t`].
120#[no_mangle]
121pub unsafe extern "C" fn mxr_select_audio_source_by_name(
122    remote: *const mxr_remote_t,
123    sink: mxr_bay_uid_t,
124    name: *const c_char,
125    format: *const mxr_audio_format_t,
126) -> mxr_result_t {
127    // SAFETY: the caller guarantees a live handle or null.
128    let handle = unsafe { remote.as_ref() };
129    with(handle, |r| {
130        // SAFETY: the caller guarantees a NUL-terminated string.
131        let name = match unsafe { req_str(name) } {
132            Ok(name) => name,
133            Err(code) => return code,
134        };
135        // SAFETY: the caller guarantees an initialised struct or null.
136        let format = unsafe { format.as_ref() }.map(|f| (*f).into());
137        from_control(
138            r.remote
139                .select_audio_source_by_name(sink.into(), name, format),
140        )
141    })
142}
143
144/// Routes a V2IP sink's audio to a multicast group directly, for a source this
145/// client has not heard advertise it.
146///
147/// `audio_port` may be zero for the default, and `format` may be null to leave
148/// the sink's audio format alone.
149///
150/// # Safety
151///
152/// `remote` is null or a live handle, `audio_ip` is a NUL-terminated dotted
153/// quad, and `format` is null or points at an initialised
154/// [`mxr_audio_format_t`].
155#[no_mangle]
156pub unsafe extern "C" fn mxr_select_audio_source_addr(
157    remote: *const mxr_remote_t,
158    sink: mxr_bay_uid_t,
159    audio_ip: *const c_char,
160    audio_port: u16,
161    format: *const mxr_audio_format_t,
162) -> mxr_result_t {
163    // SAFETY: the caller guarantees a live handle or null.
164    let handle = unsafe { remote.as_ref() };
165    with(handle, |r| {
166        // SAFETY: the caller guarantees a NUL-terminated string.
167        let text = match unsafe { req_str(audio_ip) } {
168            Ok(text) => text,
169            Err(code) => return code,
170        };
171        let Ok(ip) = text.parse() else {
172            return fail(
173                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
174                &format!("audio_ip is not an IPv4 address: {text:?}"),
175            );
176        };
177        // SAFETY: the caller guarantees an initialised struct or null.
178        let format = unsafe { format.as_ref() }.map(|f| (*f).into());
179        from_control(r.remote.select_audio_source_addr(
180            sink.into(),
181            ip,
182            // Zero is not a port a stream can arrive on, so it is how the
183            // caller declines to name one.
184            (audio_port != 0).then_some(audio_port),
185            format,
186        ))
187    })
188}
189
190/// One stream of a route the caller assembles.
191#[repr(C)]
192#[derive(Clone, Copy)]
193pub struct mxr_stream_addr_t {
194    /// The multicast group, as a dotted quad. Null or empty sends the slot
195    /// zeroed, naming no group for that stream.
196    ///
197    /// It is not a way to leave one stream alone. The firmware decides
198    /// whether a sink has a manual route at all by reading the video and
199    /// ancillary slots, so an empty one of those disqualifies the whole
200    /// route rather than preserving anything - see
201    /// `mxr_select_source_addr()`.
202    pub ip: *const c_char,
203    /// The destination UDP port. Zero means the standard port for the stream
204    /// this slot names.
205    pub port: u16,
206}
207
208/// The three streams a manual route points a V2IP sink at.
209#[repr(C)]
210#[derive(Clone, Copy)]
211pub struct mxr_v2ip_route_t {
212    /// The video stream, at port 50020 unless the port says otherwise.
213    pub video: mxr_stream_addr_t,
214    /// The audio stream, at port 50022 unless the port says otherwise.
215    pub audio: mxr_stream_addr_t,
216    /// The ancillary-data stream, at port 50021 unless the port says
217    /// otherwise.
218    pub anc: mxr_stream_addr_t,
219}
220
221/// Reads one route slot, where a null or empty address means "not set".
222///
223/// # Safety
224///
225/// `slot.ip` is null or a NUL-terminated string.
226unsafe fn to_target(slot: mxr_stream_addr_t, what: &str) -> Result<V2ipRouteTarget, mxr_result_t> {
227    // SAFETY: the caller guarantees a NUL-terminated string or null.
228    let text = unsafe { opt_str(slot.ip) }?.unwrap_or_default();
229    if text.is_empty() {
230        return Ok(V2ipRouteTarget::default());
231    }
232    match text.parse() {
233        Ok(ip) => Ok(V2ipRouteTarget {
234            ip,
235            port: slot.port,
236        }),
237        Err(_) => Err(fail(
238            mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
239            &format!("{what} is not an IPv4 address: {text:?}"),
240        )),
241    }
242}
243
244/// Routes a V2IP sink's video, audio and ancillary streams to multicast groups
245/// the caller names.
246///
247/// This is the only way to reach a stream no device on the mesh advertises,
248/// such as one the calling program is transmitting itself; the routes by
249/// source port and by name can only name a stream some bay has announced.
250///
251/// Set all three groups. The firmware decides whether a sink has a manual
252/// route by looking at the video and ancillary groups, so a route that leaves
253/// either unset does not register as one and the sink falls back to the audio
254/// source its mesh picks.
255///
256/// A null `format` sends 48kHz stereo rather than omitting the field. The
257/// firmware stores whatever the frame carries and hands it to the FPGA
258/// unexamined, so a frame without a format leaves a zero sample rate there,
259/// which the FPGA rejects and which takes the switch down with it.
260///
261/// # Safety
262///
263/// `remote` is null or a live handle, `route` points at an initialised
264/// [`mxr_v2ip_route_t`] whose addresses are null or NUL-terminated strings,
265/// and `format` is null or points at an initialised [`mxr_audio_format_t`].
266#[no_mangle]
267pub unsafe extern "C" fn mxr_select_source_addr(
268    remote: *const mxr_remote_t,
269    sink: mxr_bay_uid_t,
270    route: *const mxr_v2ip_route_t,
271    format: *const mxr_audio_format_t,
272) -> mxr_result_t {
273    // SAFETY: the caller guarantees a live handle or null.
274    let handle = unsafe { remote.as_ref() };
275    with(handle, |r| {
276        // SAFETY: the caller guarantees an initialised struct or null.
277        let Some(route) = (unsafe { route.as_ref() }) else {
278            return fail(
279                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
280                "the route pointer is null",
281            );
282        };
283        // SAFETY: the caller guarantees NUL-terminated strings or null.
284        let route = unsafe {
285            match (
286                to_target(route.video, "video ip"),
287                to_target(route.audio, "audio ip"),
288                to_target(route.anc, "anc ip"),
289            ) {
290                (Ok(video), Ok(audio), Ok(anc)) => V2ipRoute { video, audio, anc },
291                (Err(code), _, _) | (_, Err(code), _) | (_, _, Err(code)) => return code,
292            }
293        };
294        // SAFETY: the caller guarantees an initialised struct or null.
295        let format = unsafe { format.as_ref() }.map(|f| (*f).into());
296        from_control(r.remote.select_source_addr(sink.into(), route, format))
297    })
298}
299
300// ---- bays ----
301
302/// Renames a bay. The device stores the first 16 bytes.
303///
304/// # Safety
305///
306/// `remote` is null or a live handle, and `name` is a NUL-terminated string.
307#[no_mangle]
308pub unsafe extern "C" fn mxr_set_bay_name(
309    remote: *const mxr_remote_t,
310    bay: mxr_bay_uid_t,
311    name: *const c_char,
312) -> mxr_result_t {
313    // SAFETY: the caller guarantees a live handle or null.
314    let handle = unsafe { remote.as_ref() };
315    with(handle, |r| {
316        // SAFETY: the caller guarantees a NUL-terminated string.
317        match unsafe { req_str(name) } {
318            Ok(name) => from_control(r.remote.set_bay_name(bay.into(), name)),
319            Err(code) => code,
320        }
321    })
322}
323
324/// Hides a bay from the installation's user interface, or shows it again.
325///
326/// # Safety
327///
328/// `remote` is null or a live handle from `mxr_remote_new()`.
329#[no_mangle]
330pub unsafe extern "C" fn mxr_set_bay_hidden(
331    remote: *const mxr_remote_t,
332    bay: mxr_bay_uid_t,
333    hidden: bool,
334) -> mxr_result_t {
335    // SAFETY: the caller guarantees a live handle or null.
336    let handle = unsafe { remote.as_ref() };
337    with(handle, |r| {
338        from_control(r.remote.set_bay_hidden(bay.into(), hidden))
339    })
340}
341
342/// Switches an input bay's EDID profile.
343///
344/// # Safety
345///
346/// `remote` is null or a live handle from `mxr_remote_new()`.
347#[no_mangle]
348pub unsafe extern "C" fn mxr_select_edid_profile(
349    remote: *const mxr_remote_t,
350    bay: mxr_bay_uid_t,
351    profile: u16,
352) -> mxr_result_t {
353    // SAFETY: the caller guarantees a live handle or null.
354    let handle = unsafe { remote.as_ref() };
355    with(handle, |r| {
356        from_control(
357            r.remote
358                .select_edid_profile(bay.into(), EdidProfile::from_wire(profile)),
359        )
360    })
361}
362
363/// Sends a remote-control action to whatever is attached to a bay.
364///
365/// # Safety
366///
367/// `remote` is null or a live handle from `mxr_remote_new()`.
368#[no_mangle]
369pub unsafe extern "C" fn mxr_send_action(
370    remote: *const mxr_remote_t,
371    bay: mxr_bay_uid_t,
372    action: u16,
373) -> mxr_result_t {
374    // SAFETY: the caller guarantees a live handle or null.
375    let handle = unsafe { remote.as_ref() };
376    with(handle, |r| {
377        from_control(
378            r.remote
379                .send_action(bay.into(), RcAction::from_wire(action)),
380        )
381    })
382}
383
384/// Sends a remote-control key press to whatever is attached to a bay.
385///
386/// The device forwards it over CEC, infrared or IP, whichever that bay is
387/// configured for; the caller does not choose. `key` is one of the `MXR_KEY_*`
388/// values, or a raw code above `MXR_KEY_CUSTOM_CEC` or `MXR_KEY_CUSTOM_SKY`.
389/// A value this library has no name for is sent as it was given.
390///
391/// `mxr_send_action()` names an outcome instead, and lets the device decide
392/// which keys reach it.
393///
394/// # Safety
395///
396/// `remote` is null or a live handle from `mxr_remote_new()`.
397#[no_mangle]
398pub unsafe extern "C" fn mxr_send_key(
399    remote: *const mxr_remote_t,
400    bay: mxr_bay_uid_t,
401    key: u16,
402) -> mxr_result_t {
403    // SAFETY: the caller guarantees a live handle or null.
404    let handle = unsafe { remote.as_ref() };
405    with(handle, |r| {
406        from_control(r.remote.send_key(bay.into(), RcKey::from_wire(key)))
407    })
408}
409
410/// Powers on what is attached to a bay.
411///
412/// # Safety
413///
414/// `remote` is null or a live handle from `mxr_remote_new()`.
415#[no_mangle]
416pub unsafe extern "C" fn mxr_power_on(
417    remote: *const mxr_remote_t,
418    bay: mxr_bay_uid_t,
419) -> mxr_result_t {
420    // SAFETY: the caller guarantees a live handle or null.
421    let handle = unsafe { remote.as_ref() };
422    with(handle, |r| from_control(r.remote.power_on(bay.into())))
423}
424
425/// Powers off what is attached to a bay.
426///
427/// # Safety
428///
429/// `remote` is null or a live handle from `mxr_remote_new()`.
430#[no_mangle]
431pub unsafe extern "C" fn mxr_power_off(
432    remote: *const mxr_remote_t,
433    bay: mxr_bay_uid_t,
434) -> mxr_result_t {
435    // SAFETY: the caller guarantees a live handle or null.
436    let handle = unsafe { remote.as_ref() };
437    with(handle, |r| from_control(r.remote.power_off(bay.into())))
438}
439
440// ---- volume ----
441
442/// Sets a bay's volume percentage, and its mute state when `muted` is not
443/// `MXR_UNKNOWN`.
444///
445/// A bay with no volume control of its own is set through its `linked_bay`,
446/// so an output wired to an amplifier zone reaches that zone.
447///
448/// # Safety
449///
450/// `remote` is null or a live handle from `mxr_remote_new()`.
451#[no_mangle]
452pub unsafe extern "C" fn mxr_set_volume(
453    remote: *const mxr_remote_t,
454    bay: mxr_bay_uid_t,
455    volume: u8,
456    muted: mxr_tribool_t,
457) -> mxr_result_t {
458    // SAFETY: the caller guarantees a live handle or null.
459    let handle = unsafe { remote.as_ref() };
460    with(handle, |r| {
461        from_control(r.remote.set_volume(
462            bay.into(),
463            volume,
464            match muted {
465                mxr_tribool_t::MXR_UNKNOWN => None,
466                mxr_tribool_t::MXR_FALSE => Some(false),
467                mxr_tribool_t::MXR_TRUE => Some(true),
468            },
469        ))
470    })
471}
472
473/// Asks a bay to step its volume up.
474///
475/// # Safety
476///
477/// `remote` is null or a live handle from `mxr_remote_new()`.
478#[no_mangle]
479pub unsafe extern "C" fn mxr_volume_up(
480    remote: *const mxr_remote_t,
481    bay: mxr_bay_uid_t,
482) -> mxr_result_t {
483    // SAFETY: the caller guarantees a live handle or null.
484    let handle = unsafe { remote.as_ref() };
485    with(handle, |r| from_control(r.remote.volume_up(bay.into())))
486}
487
488/// Asks a bay to step its volume down.
489///
490/// # Safety
491///
492/// `remote` is null or a live handle from `mxr_remote_new()`.
493#[no_mangle]
494pub unsafe extern "C" fn mxr_volume_down(
495    remote: *const mxr_remote_t,
496    bay: mxr_bay_uid_t,
497) -> mxr_result_t {
498    // SAFETY: the caller guarantees a live handle or null.
499    let handle = unsafe { remote.as_ref() };
500    with(handle, |r| from_control(r.remote.volume_down(bay.into())))
501}
502
503/// Mutes or unmutes a bay, leaving its volume alone.
504///
505/// # Safety
506///
507/// `remote` is null or a live handle from `mxr_remote_new()`.
508#[no_mangle]
509pub unsafe extern "C" fn mxr_set_muted(
510    remote: *const mxr_remote_t,
511    bay: mxr_bay_uid_t,
512    muted: bool,
513) -> mxr_result_t {
514    // SAFETY: the caller guarantees a live handle or null.
515    let handle = unsafe { remote.as_ref() };
516    with(handle, |r| {
517        from_control(r.remote.set_muted(bay.into(), muted))
518    })
519}
520
521/// Writes an amplifier zone's gain, delay, tone and power settings.
522///
523/// This replaces every setting at once, so a caller changing one reads the
524/// current set with `mxr_bay_amp_settings()`
525/// first.
526///
527/// # Safety
528///
529/// `remote` is null or a live handle, and `settings` points at an initialised
530/// [`mxr_amp_zone_settings_t`].
531#[no_mangle]
532pub unsafe extern "C" fn mxr_set_amp_zone_settings(
533    remote: *const mxr_remote_t,
534    bay: mxr_bay_uid_t,
535    settings: *const mxr_amp_zone_settings_t,
536) -> mxr_result_t {
537    // SAFETY: the caller guarantees a live handle or null.
538    let handle = unsafe { remote.as_ref() };
539    with(handle, |r| {
540        // SAFETY: the caller guarantees an initialised struct or null.
541        let Some(settings) = (unsafe { settings.as_ref() }) else {
542            return fail(
543                mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
544                "the amp zone settings pointer is null",
545            );
546        };
547        from_control(
548            r.remote
549                .set_amp_zone_settings(bay.into(), (*settings).into()),
550        )
551    })
552}
553
554// ---- audio endpoints ----
555
556/// Mutes or unmutes one of a device's audio endpoints.
557///
558/// A loadable module serves this, not the device firmware, and a model may
559/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
560/// sent and not that anything acted on it.
561///
562/// # Safety
563///
564/// `remote` is null or a live handle from `mxr_remote_new()`.
565#[no_mangle]
566pub unsafe extern "C" fn mxr_set_audio_endpoint_muted(
567    remote: *const mxr_remote_t,
568    device: mxr_uid_t,
569    endpoint: u16,
570    muted: bool,
571) -> mxr_result_t {
572    // SAFETY: the caller guarantees a live handle or null.
573    let handle = unsafe { remote.as_ref() };
574    with(handle, |r| {
575        from_control(
576            r.remote
577                .set_audio_endpoint_muted(device.into(), endpoint, muted),
578        )
579    })
580}
581
582/// Activates or clears an audio endpoint's trigger.
583///
584/// A loadable module serves this, not the device firmware, and a model may
585/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
586/// sent and not that anything acted on it.
587///
588/// # Safety
589///
590/// `remote` is null or a live handle from `mxr_remote_new()`.
591#[no_mangle]
592pub unsafe extern "C" fn mxr_set_audio_endpoint_trigger(
593    remote: *const mxr_remote_t,
594    device: mxr_uid_t,
595    endpoint: u16,
596    active: bool,
597) -> mxr_result_t {
598    // SAFETY: the caller guarantees a live handle or null.
599    let handle = unsafe { remote.as_ref() };
600    with(handle, |r| {
601        from_control(
602            r.remote
603                .set_audio_endpoint_trigger(device.into(), endpoint, active),
604        )
605    })
606}
607
608/// Sets an audio endpoint's volume.
609///
610/// A loadable module serves this, not the device firmware, and a model may
611/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
612/// sent and not that anything acted on it.
613///
614/// # Safety
615///
616/// `remote` is null or a live handle from `mxr_remote_new()`.
617#[no_mangle]
618pub unsafe extern "C" fn mxr_set_audio_endpoint_volume(
619    remote: *const mxr_remote_t,
620    device: mxr_uid_t,
621    endpoint: u16,
622    volume: u32,
623) -> mxr_result_t {
624    // SAFETY: the caller guarantees a live handle or null.
625    let handle = unsafe { remote.as_ref() };
626    with(handle, |r| {
627        from_control(
628            r.remote
629                .set_audio_endpoint_volume(device.into(), endpoint, volume),
630        )
631    })
632}
633
634/// Points one device's audio endpoint at another device's.
635///
636/// `sink` is the end doing the listening and `source` the end being
637/// listened to.
638///
639/// A loadable module serves this, not the device firmware, and a model may
640/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
641/// sent and not that anything acted on it.
642///
643/// # Safety
644///
645/// `remote` is null or a live handle from `mxr_remote_new()`.
646#[no_mangle]
647pub unsafe extern "C" fn mxr_select_audio_endpoint_input(
648    remote: *const mxr_remote_t,
649    sink: mxr_uid_t,
650    sink_endpoint: u16,
651    source: mxr_uid_t,
652    source_endpoint: u16,
653) -> mxr_result_t {
654    // SAFETY: the caller guarantees a live handle or null.
655    let handle = unsafe { remote.as_ref() };
656    with(handle, |r| {
657        from_control(r.remote.select_audio_endpoint_input(
658            sink.into(),
659            sink_endpoint,
660            source.into(),
661            source_endpoint,
662        ))
663    })
664}
665
666// ---- devices ----
667
668/// Asks a device for an EDID: the one the display on its output publishes, or
669/// the one the device presents to the source on its input.
670///
671/// The device answers a moment later. The bytes reach `on_edid_received` and
672/// stay readable through `mxr_device_edid()`.
673///
674/// Only V2IP hardware handles this opcode. A matrix or an amplifier accepts
675/// the frame and answers nothing, at any protocol version, so the silence that
676/// follows is permanent rather than a reply still to come. `MXR_OK` here means
677/// the frame was sent, and nothing more; a caller polling for an EDID should
678/// ask a device that can answer rather than wait on one that cannot.
679///
680/// # Safety
681///
682/// `remote` is null or a live handle from `mxr_remote_new()`.
683#[no_mangle]
684pub unsafe extern "C" fn mxr_request_edid(
685    remote: *const mxr_remote_t,
686    device: mxr_uid_t,
687    output: bool,
688) -> mxr_result_t {
689    // SAFETY: the caller guarantees a live handle or null.
690    let handle = unsafe { remote.as_ref() };
691    with(handle, |r| {
692        from_control(r.remote.request_edid(device.into(), output))
693    })
694}
695
696/// Asks for a detailed signal report from every bay of one device, or - with
697/// the zero uid - from every bay on the network.
698///
699/// Devices report on their own when a signal changes, so this is what a client
700/// that has just started needs: without it, a bay that has been showing the
701/// same picture for an hour says nothing until it changes.
702///
703/// # Safety
704///
705/// `remote` is null or a live handle from `mxr_remote_new()`.
706#[no_mangle]
707pub unsafe extern "C" fn mxr_request_signal_status(
708    remote: *const mxr_remote_t,
709    device: mxr_uid_t,
710) -> mxr_result_t {
711    // SAFETY: the caller guarantees a live handle or null.
712    let handle = unsafe { remote.as_ref() };
713    with(handle, |r| {
714        let uid = DeviceUid::from(device);
715        let target = (uid != DeviceUid::ZERO).then_some(uid);
716        from_control(r.remote.request_signal_status(target))
717    })
718}
719
720/// Subscribes to, or unsubscribes from, a device's V2IP statistics.
721///
722/// # Safety
723///
724/// `remote` is null or a live handle from `mxr_remote_new()`.
725#[no_mangle]
726pub unsafe extern "C" fn mxr_subscribe_v2ip_stats(
727    remote: *const mxr_remote_t,
728    device: mxr_uid_t,
729    subscribe: bool,
730) -> mxr_result_t {
731    // SAFETY: the caller guarantees a live handle or null.
732    let handle = unsafe { remote.as_ref() };
733    with(handle, |r| {
734        from_control(r.remote.subscribe_v2ip_stats(device.into(), subscribe))
735    })
736}
737
738/// Reboots a device.
739///
740/// # Safety
741///
742/// `remote` is null or a live handle from `mxr_remote_new()`.
743#[no_mangle]
744pub unsafe extern "C" fn mxr_reboot(
745    remote: *const mxr_remote_t,
746    device: mxr_uid_t,
747) -> mxr_result_t {
748    // SAFETY: the caller guarantees a live handle or null.
749    let handle = unsafe { remote.as_ref() };
750    with(handle, |r| from_control(r.remote.reboot(device.into())))
751}
752
753/// Sends the monitoring pulse that tells devices this client is watching.
754///
755/// # Safety
756///
757/// `remote` is null or a live handle from `mxr_remote_new()`.
758#[no_mangle]
759pub unsafe extern "C" fn mxr_send_monitoring_pulse(remote: *const mxr_remote_t) -> mxr_result_t {
760    // SAFETY: the caller guarantees a live handle or null.
761    let handle = unsafe { remote.as_ref() };
762    with(handle, |r| from_control(r.remote.send_monitoring_pulse()))
763}
764
765// ---- V2IP scaling ----
766
767/// The colour space a V2IP sink scales its output to.
768pub const MXR_V2IP_COLOUR_RGB: u8 = 0;
769/// YCbCr 4:4:4. See `MXR_V2IP_COLOUR_RGB`.
770pub const MXR_V2IP_COLOUR_YCBCR444: u8 = 1;
771/// YCbCr 4:2:2. See `MXR_V2IP_COLOUR_RGB`.
772pub const MXR_V2IP_COLOUR_YCBCR422: u8 = 2;
773/// YCbCr 4:2:0. See `MXR_V2IP_COLOUR_RGB`.
774pub const MXR_V2IP_COLOUR_YCBCR420: u8 = 3;
775
776/// Lowest refresh rate a V2IP output stage accepts, in Hz.
777pub const MXR_V2IP_SCALING_REFRESH_MIN: u16 = 24;
778
779/// Highest refresh rate a V2IP output stage accepts, in Hz.
780pub const MXR_V2IP_SCALING_REFRESH_MAX: u16 = 120;
781
782/// The output format to scale a V2IP sink to.
783///
784/// Given as a depth and a colour space rather than as a packed signal-type
785/// word, so a caller cannot send the "no depth" index a sink reports while it
786/// has none configured - a value a sink decodes cleanly and then drops.
787///
788/// `svd` must name a known video descriptor and may not be zero, `depth` must
789/// be 8, 10 or 12, `colour` one of the `MXR_V2IP_COLOUR_*` values, and
790/// `refresh` between `MXR_V2IP_SCALING_REFRESH_MIN` and
791/// `MXR_V2IP_SCALING_REFRESH_MAX`. Each is checked before anything is sent.
792#[repr(C)]
793#[derive(Clone, Copy)]
794pub struct mxr_v2ip_output_mode_t {
795    /// The CTA-861 short video descriptor to output.
796    pub svd: u8,
797    /// Bit depth: 8, 10 or 12.
798    pub depth: u8,
799    /// One of the `MXR_V2IP_COLOUR_*` values.
800    pub colour: u8,
801    /// Refresh rate in Hz.
802    pub refresh: u16,
803}
804
805impl From<mxr_v2ip_output_mode_t> for V2ipOutputMode {
806    fn from(m: mxr_v2ip_output_mode_t) -> Self {
807        Self {
808            svd: m.svd,
809            depth: m.depth,
810            colour: V2ipColourSpace::from_wire(m.colour),
811            refresh: m.refresh,
812        }
813    }
814}
815
816/// Reads a mode argument, refusing a null pointer.
817unsafe fn output_mode(mode: *const mxr_v2ip_output_mode_t) -> Result<V2ipOutputMode, mxr_result_t> {
818    // SAFETY: the caller guarantees an initialised struct or null.
819    match unsafe { mode.as_ref() } {
820        Some(m) => Ok((*m).into()),
821        None => Err(fail(
822            mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
823            "the mode pointer is null",
824        )),
825    }
826}
827
828/// Turns a V2IP sink's automatic scaling on or off.
829///
830/// Automatic scaling and a configured output mode are separate reasons for a
831/// sink to scale, and this moves only the first: a sink with a mode configured
832/// goes on scaling to it with automatic scaling off. Turning both off is this
833/// call plus `mxr_clear_v2ip_output_mode()`.
834///
835/// Nothing acknowledges the frame, so `MXR_OK` means it was sent. Read the sink
836/// back with `mxr_v2ip_details()`, and trust the scaling fields only where the
837/// device reports `MXR_FEATURE_CONFIG_INITIALISED`.
838///
839/// **Read any route you still need before writing.** The sink rebuilds and
840/// rebroadcasts its subscription in response, and the addresses in
841/// `mxr_v2ip_details()` can read as zero for up to a minute afterwards.
842///
843/// # Safety
844///
845/// `remote` is null or a live handle from `mxr_remote_new()`.
846#[no_mangle]
847pub unsafe extern "C" fn mxr_set_v2ip_auto_scaling(
848    remote: *const mxr_remote_t,
849    device: mxr_uid_t,
850    enabled: bool,
851) -> mxr_result_t {
852    // SAFETY: the caller guarantees a live handle or null.
853    let handle = unsafe { remote.as_ref() };
854    with(handle, |r| {
855        from_control(r.remote.set_v2ip_auto_scaling(device.into(), enabled))
856    })
857}
858
859/// Switches on/off settings of a V2IP device, all to the same value.
860///
861/// `setting` is one or more `MXR_V2IP_SETTING_*` on/off bits, each one the
862/// device has reported. Returns `MXR_ERR_NOT_REPORTED` before the device has
863/// reported its settings, `MXR_ERR_UNSUPPORTED` for a setting it does not have
864/// and `MXR_ERR_INVALID_ARGUMENT` for a bit that is not an on/off setting,
865/// sending nothing in each case: the device ignores such a write. Until the
866/// device reports back, `mxr_v2ip_device_settings()` reads what was written.
867///
868/// # Safety
869///
870/// `remote` is null or a live handle from `mxr_remote_new()`.
871#[no_mangle]
872pub unsafe extern "C" fn mxr_set_v2ip_device_setting(
873    remote: *const mxr_remote_t,
874    device: mxr_uid_t,
875    setting: u32,
876    enabled: bool,
877) -> mxr_result_t {
878    // SAFETY: the caller guarantees a live handle or null.
879    let handle = unsafe { remote.as_ref() };
880    with(handle, |r| {
881        from_control(r.remote.set_v2ip_device_setting(
882            device.into(),
883            V2ipDeviceSetting::from_bits(setting),
884            enabled,
885        ))
886    })
887}
888
889/// Sets the infrared profile of a V2IP device's global infrared port.
890///
891/// `profile` is from 0 up to, not including, `MXR_V2IP_IR_PROFILE_MAX`.
892/// Otherwise as `mxr_set_v2ip_device_setting()`.
893///
894/// # Safety
895///
896/// `remote` is null or a live handle from `mxr_remote_new()`.
897#[no_mangle]
898pub unsafe extern "C" fn mxr_set_v2ip_ir_profile(
899    remote: *const mxr_remote_t,
900    device: mxr_uid_t,
901    profile: i8,
902) -> mxr_result_t {
903    // SAFETY: the caller guarantees a live handle or null.
904    let handle = unsafe { remote.as_ref() };
905    with(handle, |r| {
906        from_control(r.remote.set_v2ip_ir_profile(device.into(), profile))
907    })
908}
909
910/// Sets the infrared profile of a V2IP device's output infrared port.
911///
912/// `MXR_V2IP_IR_PROFILE_NOT_SET` makes the port follow the global one.
913/// Otherwise as `mxr_set_v2ip_ir_profile()`.
914///
915/// # Safety
916///
917/// `remote` is null or a live handle from `mxr_remote_new()`.
918#[no_mangle]
919pub unsafe extern "C" fn mxr_set_v2ip_sink_ir_profile(
920    remote: *const mxr_remote_t,
921    device: mxr_uid_t,
922    profile: i8,
923) -> mxr_result_t {
924    // SAFETY: the caller guarantees a live handle or null.
925    let handle = unsafe { remote.as_ref() };
926    with(handle, |r| {
927        from_control(r.remote.set_v2ip_sink_ir_profile(device.into(), profile))
928    })
929}
930
931/// Sets the output format a V2IP sink scales to.
932///
933/// The mode is checked here and `MXR_ERR_INVALID_ARGUMENT` returned without
934/// sending anything, because a sink refuses a bad one in silence. Passing is
935/// not a guarantee: the sink also weighs the format against the attached
936/// display's EDID and against what its own output stage can produce.
937///
938/// **Turn automatic scaling off first if it is on.** A sink silently refuses a
939/// mode the display does not list while it is scaling automatically. Set the
940/// mode, then turn automatic scaling back on if it was on.
941///
942/// **Pass an `svd` and a `refresh` that agree.** A sink stores both halves and,
943/// with its match-source setting on as it ships, reports back the SVD matching
944/// the refresh it holds: a 60Hz SVD written with a refresh of 50 reads back as
945/// that SVD's 50Hz sibling, once, and stays there. A sink with match-source off
946/// reports the SVD it was given. Either way a pair that agrees reads back
947/// unchanged and the format driven is the same, and the substitution appears on
948/// the sink's next report rather than in the next `mxr_v2ip_details()`.
949///
950/// A mode read from the sink's own web interface is not interchangeable with
951/// this pair. That interface reports the SVD's 60Hz sibling and carries the
952/// refresh in a field of its own, so writing back what it shows as the mode, on
953/// its own, changes the setting rather than restoring it.
954///
955/// **Read any route you still need before writing.** The sink rebuilds and
956/// rebroadcasts its subscription in response, and the addresses in
957/// `mxr_v2ip_details()` can read as zero for up to a minute afterwards.
958///
959/// # Safety
960///
961/// `remote` is null or a live handle, and `mode` points at an initialised
962/// [`mxr_v2ip_output_mode_t`].
963#[no_mangle]
964pub unsafe extern "C" fn mxr_set_v2ip_output_mode(
965    remote: *const mxr_remote_t,
966    device: mxr_uid_t,
967    mode: *const mxr_v2ip_output_mode_t,
968) -> mxr_result_t {
969    // SAFETY: the caller guarantees a live handle or null.
970    let handle = unsafe { remote.as_ref() };
971    with(handle, |r| {
972        // SAFETY: the caller guarantees an initialised struct or null.
973        match unsafe { output_mode(mode) } {
974            Ok(m) => from_control(r.remote.set_v2ip_output_mode(device.into(), m)),
975            Err(code) => code,
976        }
977    })
978}
979
980/// Clears the output format a V2IP sink is configured to scale to.
981///
982/// The sink stops scaling for that reason and keeps its automatic scaling
983/// setting. This is the only way to express "no mode configured", and it is
984/// what restoring a sink that had none requires: a sink reports no mode by
985/// leaving `MXR_SCALING_FLAG_MODE_VALID` clear, which a write cannot say.
986///
987/// **Read any route you still need before writing.** The sink rebuilds and
988/// rebroadcasts its subscription in response, and the addresses in
989/// `mxr_v2ip_details()` can read as zero for up to a minute afterwards.
990///
991/// # Safety
992///
993/// `remote` is null or a live handle from `mxr_remote_new()`.
994#[no_mangle]
995pub unsafe extern "C" fn mxr_clear_v2ip_output_mode(
996    remote: *const mxr_remote_t,
997    device: mxr_uid_t,
998) -> mxr_result_t {
999    // SAFETY: the caller guarantees a live handle or null.
1000    let handle = unsafe { remote.as_ref() };
1001    with(handle, |r| {
1002        from_control(r.remote.clear_v2ip_output_mode(device.into()))
1003    })
1004}
1005
1006// ---- video wall ----
1007
1008/// Where a video-wall sink's window sits, and the picture it was measured
1009/// against.
1010///
1011/// `pos_x` must be a multiple of `MXR_VIDEO_WALL_POS_ALIGN`, `width` a
1012/// multiple of `MXR_VIDEO_WALL_WIDTH_ALIGN`, both sides at least
1013/// `MXR_VIDEO_WALL_MIN_SIZE`, and the window must fit inside the raster it
1014/// names. `pos_y` and `height` have no alignment rule. A zero `width` or
1015/// `height` clears the wall and is checked against none of this.
1016#[repr(C)]
1017#[derive(Clone, Copy)]
1018pub struct mxr_video_wall_window_t {
1019    /// Window origin, horizontal.
1020    pub pos_x: u16,
1021    /// Window origin, vertical.
1022    pub pos_y: u16,
1023    /// Window width, or zero to clear the wall.
1024    pub width: u16,
1025    /// Window height, or zero to clear the wall.
1026    pub height: u16,
1027    /// Active picture width the window was measured against.
1028    pub raster_w: u16,
1029    /// Active picture height the window was measured against.
1030    pub raster_h: u16,
1031}
1032
1033/// A window's horizontal origin must be a multiple of this.
1034pub const MXR_VIDEO_WALL_POS_ALIGN: u16 = 64;
1035
1036/// A window's width must be a multiple of this.
1037pub const MXR_VIDEO_WALL_WIDTH_ALIGN: u16 = 4;
1038
1039/// Neither side of a window may be smaller than this.
1040pub const MXR_VIDEO_WALL_MIN_SIZE: u16 = 64;
1041
1042impl From<mxr_video_wall_window_t> for VideoWallWindow {
1043    fn from(w: mxr_video_wall_window_t) -> Self {
1044        Self {
1045            pos_x: w.pos_x,
1046            pos_y: w.pos_y,
1047            width: w.width,
1048            height: w.height,
1049            raster_w: w.raster_w,
1050            raster_h: w.raster_h,
1051        }
1052    }
1053}
1054
1055/// Reads a window argument, refusing a null pointer.
1056unsafe fn wall_window(
1057    window: *const mxr_video_wall_window_t,
1058) -> Result<VideoWallWindow, mxr_result_t> {
1059    // SAFETY: the caller guarantees an initialised struct or null.
1060    match unsafe { window.as_ref() } {
1061        Some(w) => Ok((*w).into()),
1062        None => Err(fail(
1063            mxr_result_t::MXR_ERR_INVALID_ARGUMENT,
1064            "the window pointer is null",
1065        )),
1066    }
1067}
1068
1069/// Shows a window on a sink's video wall without storing it.
1070///
1071/// The window lasts until the sink is told otherwise or restarts;
1072/// `mxr_revert_video_wall()` puts back whatever it has stored. A zero width or
1073/// height shows the whole frame again.
1074///
1075/// The geometry is checked here and `MXR_ERR_INVALID_ARGUMENT` returned
1076/// without sending anything, because the sink is not guaranteed to check it
1077/// itself.
1078///
1079/// A loadable module serves this, not the device firmware, and a model may
1080/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1081/// sent and not that anything acted on it.
1082///
1083/// # Safety
1084///
1085/// `remote` is null or a live handle, and `window` points at an initialised
1086/// [`mxr_video_wall_window_t`].
1087#[no_mangle]
1088pub unsafe extern "C" fn mxr_preview_video_wall(
1089    remote: *const mxr_remote_t,
1090    sink: mxr_uid_t,
1091    window: *const mxr_video_wall_window_t,
1092) -> mxr_result_t {
1093    // SAFETY: the caller guarantees a live handle or null.
1094    let handle = unsafe { remote.as_ref() };
1095    with(handle, |r| {
1096        // SAFETY: the caller guarantees an initialised struct or null.
1097        match unsafe { wall_window(window) } {
1098            Ok(w) => from_control(r.remote.preview_video_wall(sink.into(), w)),
1099            Err(code) => code,
1100        }
1101    })
1102}
1103
1104/// Stores a window as a sink's video wall.
1105///
1106/// The geometry is checked here and `MXR_ERR_INVALID_ARGUMENT` returned
1107/// without sending anything. That matters more than a refused frame would: a
1108/// sink running a video-wall module older than 2026083100 writes the window to
1109/// its configuration before asking its video processor to apply it, and the
1110/// processor's refusal does not undo the write, so an out-of-spec window
1111/// survives a reboot and is re-offered on every stream restart until something
1112/// else replaces it. A power cycle does not clear it.
1113///
1114/// A zero width or height stores "show the whole frame".
1115///
1116/// A loadable module serves this, not the device firmware, and a model may
1117/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1118/// sent and not that anything acted on it.
1119///
1120/// # Safety
1121///
1122/// `remote` is null or a live handle, and `window` points at an initialised
1123/// [`mxr_video_wall_window_t`].
1124#[no_mangle]
1125pub unsafe extern "C" fn mxr_store_video_wall(
1126    remote: *const mxr_remote_t,
1127    sink: mxr_uid_t,
1128    window: *const mxr_video_wall_window_t,
1129) -> mxr_result_t {
1130    // SAFETY: the caller guarantees a live handle or null.
1131    let handle = unsafe { remote.as_ref() };
1132    with(handle, |r| {
1133        // SAFETY: the caller guarantees an initialised struct or null.
1134        match unsafe { wall_window(window) } {
1135            Ok(w) => from_control(r.remote.store_video_wall(sink.into(), w)),
1136            Err(code) => code,
1137        }
1138    })
1139}
1140
1141/// Restores the window a sink has stored, discarding a preview.
1142///
1143/// Carries no window: the sink already holds the one this puts back.
1144///
1145/// A loadable module serves this, not the device firmware, and a model may
1146/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1147/// sent and not that anything acted on it.
1148///
1149/// # Safety
1150///
1151/// `remote` is null or a live handle from `mxr_remote_new()`.
1152#[no_mangle]
1153pub unsafe extern "C" fn mxr_revert_video_wall(
1154    remote: *const mxr_remote_t,
1155    sink: mxr_uid_t,
1156) -> mxr_result_t {
1157    // SAFETY: the caller guarantees a live handle or null.
1158    let handle = unsafe { remote.as_ref() };
1159    with(handle, |r| {
1160        from_control(r.remote.revert_video_wall(sink.into()))
1161    })
1162}
1163
1164// ---- multiviewer ----
1165
1166/// Switches a multiviewer's window layout.
1167///
1168/// A loadable module serves this, not the device firmware, and a model may
1169/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1170/// sent and not that anything acted on it.
1171///
1172/// # Safety
1173///
1174/// `remote` is null or a live handle from `mxr_remote_new()`.
1175#[no_mangle]
1176pub unsafe extern "C" fn mxr_set_multiviewer_view_mode(
1177    remote: *const mxr_remote_t,
1178    device: mxr_uid_t,
1179    mode: u8,
1180) -> mxr_result_t {
1181    // SAFETY: the caller guarantees a live handle or null.
1182    let handle = unsafe { remote.as_ref() };
1183    with(handle, |r| {
1184        from_control(
1185            r.remote
1186                .set_multiviewer_view_mode(device.into(), MultiviewerViewMode::from_wire(mode)),
1187        )
1188    })
1189}
1190
1191/// Puts a source in one of a multiviewer's windows.
1192///
1193/// A loadable module serves this, not the device firmware, and a model may
1194/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1195/// sent and not that anything acted on it.
1196///
1197/// # Safety
1198///
1199/// `remote` is null or a live handle from `mxr_remote_new()`.
1200#[no_mangle]
1201pub unsafe extern "C" fn mxr_set_multiviewer_video_source(
1202    remote: *const mxr_remote_t,
1203    device: mxr_uid_t,
1204    screen: u8,
1205    source: u8,
1206) -> mxr_result_t {
1207    // SAFETY: the caller guarantees a live handle or null.
1208    let handle = unsafe { remote.as_ref() };
1209    with(handle, |r| {
1210        from_control(r.remote.set_multiviewer_video_source(
1211            device.into(),
1212            screen,
1213            MultiviewerSource::from_wire(source),
1214        ))
1215    })
1216}
1217
1218/// Chooses which window a multiviewer takes its audio from.
1219///
1220/// A loadable module serves this, not the device firmware, and a model may
1221/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1222/// sent and not that anything acted on it.
1223///
1224/// # Safety
1225///
1226/// `remote` is null or a live handle from `mxr_remote_new()`.
1227#[no_mangle]
1228pub unsafe extern "C" fn mxr_set_multiviewer_audio_source(
1229    remote: *const mxr_remote_t,
1230    device: mxr_uid_t,
1231    source: u8,
1232) -> mxr_result_t {
1233    // SAFETY: the caller guarantees a live handle or null.
1234    let handle = unsafe { remote.as_ref() };
1235    with(handle, |r| {
1236        from_control(
1237            r.remote
1238                .set_multiviewer_audio_source(device.into(), MultiviewerSource::from_wire(source)),
1239        )
1240    })
1241}
1242
1243/// Sets a multiviewer's output volume and mute state.
1244///
1245/// A loadable module serves this, not the device firmware, and a model may
1246/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1247/// sent and not that anything acted on it.
1248///
1249/// # Safety
1250///
1251/// `remote` is null or a live handle from `mxr_remote_new()`.
1252#[no_mangle]
1253pub unsafe extern "C" fn mxr_set_multiviewer_audio_volume(
1254    remote: *const mxr_remote_t,
1255    device: mxr_uid_t,
1256    volume: u8,
1257    muted: bool,
1258) -> mxr_result_t {
1259    // SAFETY: the caller guarantees a live handle or null.
1260    let handle = unsafe { remote.as_ref() };
1261    with(handle, |r| {
1262        from_control(
1263            r.remote
1264                .set_multiviewer_audio_volume(device.into(), volume, muted),
1265        )
1266    })
1267}
1268
1269/// Switches the EDID a multiviewer presents to its sources.
1270///
1271/// A loadable module serves this, not the device firmware, and a model may
1272/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1273/// sent and not that anything acted on it.
1274///
1275/// # Safety
1276///
1277/// `remote` is null or a live handle from `mxr_remote_new()`.
1278#[no_mangle]
1279pub unsafe extern "C" fn mxr_set_multiviewer_edid_template(
1280    remote: *const mxr_remote_t,
1281    device: mxr_uid_t,
1282    template: u8,
1283) -> mxr_result_t {
1284    // SAFETY: the caller guarantees a live handle or null.
1285    let handle = unsafe { remote.as_ref() };
1286    with(handle, |r| {
1287        from_control(r.remote.set_multiviewer_edid_template(
1288            device.into(),
1289            MultiviewerEdidTemplate::from_wire(template),
1290        ))
1291    })
1292}
1293
1294/// Chooses which window a multiviewer forwards remote control to.
1295///
1296/// A loadable module serves this, not the device firmware, and a model may
1297/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1298/// sent and not that anything acted on it.
1299///
1300/// # Safety
1301///
1302/// `remote` is null or a live handle from `mxr_remote_new()`.
1303#[no_mangle]
1304pub unsafe extern "C" fn mxr_set_multiviewer_remote_control(
1305    remote: *const mxr_remote_t,
1306    device: mxr_uid_t,
1307    source: u8,
1308) -> mxr_result_t {
1309    // SAFETY: the caller guarantees a live handle or null.
1310    let handle = unsafe { remote.as_ref() };
1311    with(handle, |r| {
1312        from_control(
1313            r.remote.set_multiviewer_remote_control(
1314                device.into(),
1315                MultiviewerSource::from_wire(source),
1316            ),
1317        )
1318    })
1319}
1320
1321/// Sets the size of a multiviewer's picture-in-picture window.
1322///
1323/// A loadable module serves this, not the device firmware, and a model may
1324/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1325/// sent and not that anything acted on it.
1326///
1327/// # Safety
1328///
1329/// `remote` is null or a live handle from `mxr_remote_new()`.
1330#[no_mangle]
1331pub unsafe extern "C" fn mxr_set_multiviewer_pip_size(
1332    remote: *const mxr_remote_t,
1333    device: mxr_uid_t,
1334    size: u8,
1335) -> mxr_result_t {
1336    // SAFETY: the caller guarantees a live handle or null.
1337    let handle = unsafe { remote.as_ref() };
1338    with(handle, |r| {
1339        from_control(
1340            r.remote
1341                .set_multiviewer_pip_size(device.into(), MultiviewerPipSize::from_wire(size)),
1342        )
1343    })
1344}
1345
1346/// Sets which corner a multiviewer's picture-in-picture window sits in.
1347///
1348/// A loadable module serves this, not the device firmware, and a model may
1349/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1350/// sent and not that anything acted on it.
1351///
1352/// # Safety
1353///
1354/// `remote` is null or a live handle from `mxr_remote_new()`.
1355#[no_mangle]
1356pub unsafe extern "C" fn mxr_set_multiviewer_pip_position(
1357    remote: *const mxr_remote_t,
1358    device: mxr_uid_t,
1359    position: u8,
1360) -> mxr_result_t {
1361    // SAFETY: the caller guarantees a live handle or null.
1362    let handle = unsafe { remote.as_ref() };
1363    with(handle, |r| {
1364        from_control(r.remote.set_multiviewer_pip_position(
1365            device.into(),
1366            MultiviewerPipPosition::from_wire(position),
1367        ))
1368    })
1369}
1370
1371/// Sets how a multiviewer fits a source into its window.
1372///
1373/// A loadable module serves this, not the device firmware, and a model may
1374/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1375/// sent and not that anything acted on it.
1376///
1377/// # Safety
1378///
1379/// `remote` is null or a live handle from `mxr_remote_new()`.
1380#[no_mangle]
1381pub unsafe extern "C" fn mxr_set_multiviewer_aspect_ratio(
1382    remote: *const mxr_remote_t,
1383    device: mxr_uid_t,
1384    aspect: u8,
1385) -> mxr_result_t {
1386    // SAFETY: the caller guarantees a live handle or null.
1387    let handle = unsafe { remote.as_ref() };
1388    with(handle, |r| {
1389        from_control(
1390            r.remote.set_multiviewer_aspect_ratio(
1391                device.into(),
1392                MultiviewerAspectRatio::from_wire(aspect),
1393            ),
1394        )
1395    })
1396}
1397
1398/// Turns a multiviewer's automatic source switching on or off.
1399///
1400/// A loadable module serves this, not the device firmware, and a model may
1401/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1402/// sent and not that anything acted on it.
1403///
1404/// # Safety
1405///
1406/// `remote` is null or a live handle from `mxr_remote_new()`.
1407#[no_mangle]
1408pub unsafe extern "C" fn mxr_set_multiviewer_auto_switch(
1409    remote: *const mxr_remote_t,
1410    device: mxr_uid_t,
1411    enable: bool,
1412) -> mxr_result_t {
1413    // SAFETY: the caller guarantees a live handle or null.
1414    let handle = unsafe { remote.as_ref() };
1415    with(handle, |r| {
1416        from_control(r.remote.set_multiviewer_auto_switch(device.into(), enable))
1417    })
1418}
1419
1420/// Switches a multiviewer's output resolution.
1421///
1422/// A loadable module serves this, not the device firmware, and a model may
1423/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1424/// sent and not that anything acted on it.
1425///
1426/// # Safety
1427///
1428/// `remote` is null or a live handle from `mxr_remote_new()`.
1429#[no_mangle]
1430pub unsafe extern "C" fn mxr_set_multiviewer_output_mode(
1431    remote: *const mxr_remote_t,
1432    device: mxr_uid_t,
1433    mode: u8,
1434) -> mxr_result_t {
1435    // SAFETY: the caller guarantees a live handle or null.
1436    let handle = unsafe { remote.as_ref() };
1437    with(handle, |r| {
1438        from_control(
1439            r.remote
1440                .set_multiviewer_output_mode(device.into(), MultiviewerOutputMode::from_wire(mode)),
1441        )
1442    })
1443}
1444
1445/// Sets a multiviewer's IT content flag.
1446///
1447/// A loadable module serves this, not the device firmware, and a model may
1448/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1449/// sent and not that anything acted on it.
1450///
1451/// # Safety
1452///
1453/// `remote` is null or a live handle from `mxr_remote_new()`.
1454#[no_mangle]
1455pub unsafe extern "C" fn mxr_set_multiviewer_output_itc(
1456    remote: *const mxr_remote_t,
1457    device: mxr_uid_t,
1458    mode: u8,
1459) -> mxr_result_t {
1460    // SAFETY: the caller guarantees a live handle or null.
1461    let handle = unsafe { remote.as_ref() };
1462    with(handle, |r| {
1463        from_control(
1464            r.remote
1465                .set_multiviewer_output_itc(device.into(), MultiviewerItcMode::from_wire(mode)),
1466        )
1467    })
1468}
1469
1470/// Switches a multiviewer's HDCP mode.
1471///
1472/// A loadable module serves this, not the device firmware, and a model may
1473/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1474/// sent and not that anything acted on it.
1475///
1476/// # Safety
1477///
1478/// `remote` is null or a live handle from `mxr_remote_new()`.
1479#[no_mangle]
1480pub unsafe extern "C" fn mxr_set_multiviewer_hdcp_mode(
1481    remote: *const mxr_remote_t,
1482    device: mxr_uid_t,
1483    mode: u8,
1484) -> mxr_result_t {
1485    // SAFETY: the caller guarantees a live handle or null.
1486    let handle = unsafe { remote.as_ref() };
1487    with(handle, |r| {
1488        from_control(
1489            r.remote
1490                .set_multiviewer_hdcp_mode(device.into(), MultiviewerHdcpMode::from_wire(mode)),
1491        )
1492    })
1493}
1494
1495/// Maps one of a multiviewer's inputs to a source device.
1496///
1497/// A loadable module serves this, not the device firmware, and a model may
1498/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1499/// sent and not that anything acted on it.
1500///
1501/// # Safety
1502///
1503/// `remote` is null or a live handle from `mxr_remote_new()`.
1504#[no_mangle]
1505pub unsafe extern "C" fn mxr_set_multiviewer_input_source(
1506    remote: *const mxr_remote_t,
1507    device: mxr_uid_t,
1508    input: u8,
1509    source: mxr_uid_t,
1510) -> mxr_result_t {
1511    // SAFETY: the caller guarantees a live handle or null.
1512    let handle = unsafe { remote.as_ref() };
1513    with(handle, |r| {
1514        from_control(
1515            r.remote
1516                .set_multiviewer_input_source(device.into(), input, source.into()),
1517        )
1518    })
1519}
1520
1521/// Asks a multiviewer to map its inputs to the sources it can see.
1522///
1523/// A loadable module serves this, not the device firmware, and a model may
1524/// not have it. Nothing answers either way, so `MXR_OK` means the frame was
1525/// sent and not that anything acted on it.
1526///
1527/// # Safety
1528///
1529/// `remote` is null or a live handle from `mxr_remote_new()`.
1530#[no_mangle]
1531pub unsafe extern "C" fn mxr_multiviewer_auto_route(
1532    remote: *const mxr_remote_t,
1533    device: mxr_uid_t,
1534) -> mxr_result_t {
1535    // SAFETY: the caller guarantees a live handle or null.
1536    let handle = unsafe { remote.as_ref() };
1537    with(handle, |r| {
1538        from_control(r.remote.multiviewer_auto_route(device.into()))
1539    })
1540}