use std::os::raw::c_int;
use std::sync::mpsc;
use std::sync::{Mutex, Once};
use std::thread::{self, JoinHandle};
use pyo3::exceptions::{PyBufferError, PyRuntimeError, PyValueError};
use pyo3::ffi;
use pyo3::prelude::*;
use g2g_core::log::Target;
use g2g_core::runtime::{bounded, Receiver};
use g2g_core::{
g2g_warn, Caps, Dim, Frame, FrameTiming, G2gError, HardwareError, MemoryDomain, PropValue,
RawVideoFormat, SystemSlice,
};
use crate::cuda_plane::{nv12_planes, produced_cuda_buffer, CudaPlane};
use crate::format::format_to_py;
const CUDA_HOOK: &str = "g2g_process_cuda";
const CUDA_BATCH_HOOK: &str = "g2g_process_cuda_batch";
const CUDA_PRODUCE_HOOK: &str = "g2g_produce_cuda";
const PAYLOAD_HOOK: &str = "g2g_process_payload";
const DECLARED_PROPERTIES_HOOK: &str = "g2g_properties";
const CUDA_CONTEXT_ATTR: &str = "cuda_context";
const CUDA_DEVICE_ATTR: &str = "cuda_device";
static INIT: Once = Once::new();
enum JobKind {
Transform,
Batch,
Produce,
ProduceCuda,
}
enum JobCaps {
RawVideo {
width: u32,
height: u32,
fmt: RawVideoFormat,
},
Payload(Caps),
}
impl JobCaps {
fn video(&self) -> Option<(u32, u32, RawVideoFormat)> {
match self {
JobCaps::RawVideo { width, height, fmt } => Some((*width, *height, *fmt)),
JobCaps::Payload(_) => None,
}
}
}
struct Job {
frames: Vec<Frame>,
caps: JobCaps,
kind: JobKind,
}
type Reply = Result<Vec<Frame>, G2gError>;
#[pyclass(unsendable)]
#[derive(Debug)]
struct FrameBuffer {
ptr: *mut u8,
len: usize,
exports: core::cell::Cell<isize>,
}
#[pymethods]
impl FrameBuffer {
unsafe fn __getbuffer__(
slf: PyRefMut<'_, Self>,
view: *mut ffi::Py_buffer,
flags: c_int,
) -> PyResult<()> {
if view.is_null() {
return Err(PyBufferError::new_err("null buffer view"));
}
let ret = unsafe {
ffi::PyBuffer_FillInfo(
view,
slf.as_ptr(),
slf.ptr as *mut core::ffi::c_void,
slf.len as ffi::Py_ssize_t,
0, flags,
)
};
if ret == -1 {
Err(PyErr::take(slf.py()).unwrap_or_else(|| PyBufferError::new_err("fill failed")))
} else {
slf.exports.set(slf.exports.get() + 1);
Ok(())
}
}
unsafe fn __releasebuffer__(&self, _view: *mut ffi::Py_buffer) {
self.exports.set(self.exports.get() - 1);
}
}
#[cfg_attr(not(feature = "analytics"), allow(dead_code))]
#[derive(Debug, Clone)]
enum Staged {
Object {
label: u32,
x: f32,
y: f32,
w: f32,
h: f32,
score: f32,
},
Classification {
label: u32,
score: f32,
},
ClassNames {
names: Vec<String>,
},
Blob {
header: String,
payload: Vec<u8>,
},
Tracking {
object_id: u64,
},
Relation {
from: usize,
to: usize,
},
}
#[pyclass]
#[derive(Debug, Default)]
struct MetaSink {
staged: Mutex<Vec<Staged>>,
emitted: Mutex<Vec<Emitted>>,
}
#[derive(Debug)]
struct Emitted {
payload: Vec<u8>,
duration_ns: Option<u64>,
pts_ns: Option<u64>,
}
impl MetaSink {
fn stage(&self, item: Staged) -> usize {
let mut staged = self.staged.lock().expect("MetaSink staged lock poisoned");
staged.push(item);
staged.len() - 1
}
}
#[pymethods]
impl MetaSink {
fn add_object(&self, label: u32, x: f32, y: f32, w: f32, h: f32, score: f32) -> usize {
self.stage(Staged::Object {
label,
x,
y,
w,
h,
score,
})
}
fn add_classification(&self, label: u32, score: f32) -> usize {
self.stage(Staged::Classification { label, score })
}
fn set_class_names(&self, names: Vec<String>) {
self.stage(Staged::ClassNames { names });
}
fn add_tracking(&self, object_id: u64) -> usize {
self.stage(Staged::Tracking { object_id })
}
fn relate(&self, from: usize, to: usize) {
self.stage(Staged::Relation { from, to });
}
fn add_blob(&self, header: String, payload: Vec<u8>) {
self.stage(Staged::Blob { header, payload });
}
#[pyo3(signature = (payload, duration_ns = None, pts_ns = None))]
fn emit(&self, payload: Vec<u8>, duration_ns: Option<u64>, pts_ns: Option<u64>) {
self.emitted
.lock()
.expect("MetaSink emitted lock poisoned")
.push(Emitted {
payload,
duration_ns,
pts_ns,
});
}
}
#[pymodule(gil_used = false)]
fn g2g(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<MetaSink>()?;
m.add_class::<CudaPlane>()?;
m.add("PTS_NONE", FrameTiming::PTS_NONE)?;
Ok(())
}
pub fn init_host() {
INIT.call_once(|| {
std::env::set_var("PYML_BACKEND", "g2g");
pyo3::append_to_inittab!(g2g);
});
}
#[derive(Debug)]
pub(crate) struct PyWorker {
job_tx: Option<mpsc::Sender<Job>>,
result_rx: Receiver<Reply>,
handle: Option<JoinHandle<()>>,
}
impl PyWorker {
pub(crate) fn spawn(
module: &str,
class: &str,
draw_label: bool,
params: &[(String, PropValue)],
) -> Result<Self, G2gError> {
init_host();
let (job_tx, jobs) = mpsc::channel::<Job>();
let (results, result_rx) = bounded::<Reply>(1);
let (ack_tx, ack_rx) = mpsc::channel::<Result<(), G2gError>>();
let (m, c) = (module.to_owned(), class.to_owned());
let params = params.to_vec();
let handle = thread::Builder::new()
.name("g2g-pyworker".into())
.spawn(move || worker_main(m, c, draw_label, params, ack_tx, jobs, results))
.map_err(|_| G2gError::Hardware(HardwareError::Other))?;
match ack_rx.recv() {
Ok(Ok(())) => Ok(Self {
job_tx: Some(job_tx),
result_rx,
handle: Some(handle),
}),
Ok(Err(e)) => {
let _ = handle.join();
Err(e)
}
Err(_) => {
let _ = handle.join();
Err(G2gError::Hardware(HardwareError::Other))
}
}
}
pub(crate) async fn run(&self, frame: Frame, caps: &Caps) -> Result<Vec<Frame>, G2gError> {
self.dispatch(Job {
frames: vec![frame],
caps: job_caps(caps)?,
kind: JobKind::Transform,
})
.await
}
pub(crate) async fn run_batch(
&self,
frames: Vec<Frame>,
caps: &Caps,
) -> Result<Vec<Frame>, G2gError> {
self.dispatch(Job {
frames,
caps: job_caps(caps)?,
kind: JobKind::Batch,
})
.await
}
pub(crate) async fn run_produce_cuda(&self, caps: &Caps) -> Result<Option<Frame>, G2gError> {
let mut out = self
.dispatch(Job {
frames: Vec::new(),
caps: raw_video_caps(caps)?,
kind: JobKind::ProduceCuda,
})
.await?;
Ok(out.pop())
}
pub(crate) async fn run_produce(
&self,
frame: Frame,
caps: &Caps,
) -> Result<Option<Frame>, G2gError> {
let mut out = self
.dispatch(Job {
frames: vec![frame],
caps: raw_video_caps(caps)?,
kind: JobKind::Produce,
})
.await?;
Ok(out.pop())
}
async fn dispatch(&self, job: Job) -> Result<Vec<Frame>, G2gError> {
self.job_tx
.as_ref()
.ok_or(G2gError::Shutdown)?
.send(job)
.map_err(|_| G2gError::Shutdown)?;
self.result_rx
.recv()
.await
.unwrap_or(Err(G2gError::Shutdown))
}
}
impl Drop for PyWorker {
fn drop(&mut self) {
drop(self.job_tx.take());
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
fn worker_main(
module: String,
class: String,
draw_label: bool,
params: Vec<(String, PropValue)>,
ack: mpsc::Sender<Result<(), G2gError>>,
jobs: mpsc::Receiver<Job>,
results: g2g_core::runtime::Sender<Reply>,
) {
let instance = match Python::attach(|py| instantiate(py, &module, &class, draw_label, ¶ms))
{
Ok(obj) => {
let _ = ack.send(Ok(()));
obj
}
Err(e) => {
let _ = ack.send(Err(e));
return;
}
};
let mut emitted_sequence = 0u64;
while let Ok(job) = jobs.recv() {
let reply = Python::attach(|py| process_job(py, &instance, job, &mut emitted_sequence));
if results.try_send(reply).is_err() {
break;
}
}
Python::attach(|_py| drop(instance));
}
fn instantiate(
py: Python<'_>,
module: &str,
class: &str,
draw_label: bool,
params: &[(String, PropValue)],
) -> Result<Py<PyAny>, G2gError> {
(|| -> PyResult<Py<PyAny>> {
let m = PyModule::import(py, module)?;
let obj = m.getattr(class)?.call0()?;
obj.setattr("draw_label", draw_label)?;
let declared = declared_properties(&obj)?;
for (name, value) in params {
let attr = name.replace('-', "_");
if let Some(declared) = &declared {
if !declared.contains(&attr) {
return Err(PyValueError::new_err(format!(
"{class} has no property {name}; it declares {}",
declared.join(", ").replace('_', "-")
)));
}
}
obj.setattr(attr.as_str(), propvalue_to_py(py, value)?)?;
}
Ok(obj.unbind())
})()
.map_err(|e| py_fail(py, e))
}
fn declared_properties(obj: &Bound<'_, PyAny>) -> PyResult<Option<Vec<String>>> {
if !obj.hasattr(DECLARED_PROPERTIES_HOOK)? {
return Ok(None);
}
Ok(Some(obj.call_method0(DECLARED_PROPERTIES_HOOK)?.extract()?))
}
fn propvalue_to_py(py: Python<'_>, value: &PropValue) -> PyResult<Py<PyAny>> {
use pyo3::IntoPyObjectExt;
match value {
PropValue::Bool(b) => b.into_py_any(py),
PropValue::Int(i) => i.into_py_any(py),
PropValue::Uint(u) => u.into_py_any(py),
PropValue::Double(d) => d.into_py_any(py),
PropValue::Fraction(n, d) => (*n, *d).into_py_any(py),
PropValue::Str(s) => s.into_py_any(py),
other => Err(pyo3::exceptions::PyTypeError::new_err(format!(
"unsupported property kind {:?}",
other.kind()
))),
}
}
enum Handoff {
System(Vec<(*mut u8, usize)>),
Cuda(CudaPlane, CudaPlane),
CudaBatch(Vec<(CudaPlane, CudaPlane)>),
ProduceCuda,
}
fn handoff(py: Python<'_>, instance: &Py<PyAny>, job: &mut Job) -> Result<Handoff, G2gError> {
if matches!(job.kind, JobKind::ProduceCuda) {
require_hook(py, instance, CUDA_PRODUCE_HOOK, CUDA_REMEDY)?;
return Ok(Handoff::ProduceCuda);
}
if matches!(
job.frames.first().map(|f| &f.domain),
Some(MemoryDomain::Cuda(_))
) {
let (_, _, fmt) = job.caps.video().ok_or(G2gError::UnsupportedDomain)?;
let mut planes = Vec::with_capacity(job.frames.len());
for frame in &job.frames {
let MemoryDomain::Cuda(buf) = &frame.domain else {
return Err(G2gError::UnsupportedDomain);
};
planes.push(nv12_planes(fmt, buf).ok_or(G2gError::UnsupportedDomain)?);
}
return match job.kind {
JobKind::Transform => {
require_hook(py, instance, CUDA_HOOK, CUDA_REMEDY)?;
let (luma, chroma) = planes.pop().ok_or(G2gError::UnsupportedDomain)?;
Ok(Handoff::Cuda(luma, chroma))
}
JobKind::Batch => {
require_hook(py, instance, CUDA_BATCH_HOOK, CUDA_REMEDY)?;
Ok(Handoff::CudaBatch(planes))
}
JobKind::Produce | JobKind::ProduceCuda => Err(G2gError::UnsupportedDomain),
};
}
if matches!(job.caps, JobCaps::Payload(_)) {
require_hook(py, instance, PAYLOAD_HOOK, PAYLOAD_REMEDY)?;
}
let mut spans = Vec::with_capacity(job.frames.len());
for frame in &mut job.frames {
let MemoryDomain::System(slice) = &mut frame.domain else {
return Err(G2gError::UnsupportedDomain);
};
let bytes = slice.as_mut_slice();
spans.push((bytes.as_mut_ptr(), bytes.len()));
}
Ok(Handoff::System(spans))
}
const CUDA_REMEDY: &str = "a GPU-resident frame cannot reach it: insert cudadownload upstream";
const PAYLOAD_REMEDY: &str =
"a stream that is not raw video cannot reach it: it defines only the picture hooks";
fn require_hook(
py: Python<'_>,
instance: &Py<PyAny>,
hook: &str,
remedy: &str,
) -> Result<(), G2gError> {
let defined = instance
.bind(py)
.hasattr(hook)
.map_err(|e| py_fail(py, e))?;
if !defined {
g2g_warn!(
Target::category("pyelement"),
"hosted element defines no {hook}, so {remedy}"
);
return Err(G2gError::UnsupportedDomain);
}
Ok(())
}
fn process_job(
py: Python<'_>,
instance: &Py<PyAny>,
mut job: Job,
emitted_sequence: &mut u64,
) -> Reply {
let handoff = handoff(py, instance, &mut job)?;
let sink = match Py::new(py, MetaSink::default()) {
Ok(s) => s,
Err(e) => return Err(py_fail(py, e)),
};
let produced = match handoff {
Handoff::System(spans) => call_system(py, instance, &spans, &job, &sink),
Handoff::Cuda(luma, chroma) => call_cuda(py, instance, luma, chroma, &job, &sink),
Handoff::CudaBatch(planes) => call_cuda_batch(py, instance, planes, &job, &sink),
Handoff::ProduceCuda => call_produce_cuda(py, instance, &mut job, &sink),
};
let staged = core::mem::take(
&mut *sink
.borrow(py)
.staged
.lock()
.expect("MetaSink staged lock poisoned"),
);
let emitted = core::mem::take(
&mut *sink
.borrow(py)
.emitted
.lock()
.expect("MetaSink emitted lock poisoned"),
);
match produced {
Ok(true) => {
let frame_dims = job
.caps
.video()
.map(|(w, h, _)| (w, h))
.filter(|(w, h)| *w > 0 && *h > 0);
let Some(anchor) = job.frames.first() else {
return Ok(Vec::new());
};
let anchor_timing = anchor.timing;
let anchor_sequence = anchor.sequence;
let anchor_meta = anchor.meta.clone();
let mut out = if emitted.is_empty() {
job.frames.truncate(1);
job.frames
} else {
*emitted_sequence = (*emitted_sequence).max(anchor_sequence);
emitted
.into_iter()
.map(|emitted| {
let mut timing = anchor_timing;
if let Some(duration_ns) = emitted.duration_ns {
timing.duration_ns = duration_ns;
}
if let Some(pts_ns) = emitted.pts_ns {
timing.pts_ns = pts_ns;
timing.dts_ns = pts_ns;
}
let sequence = *emitted_sequence;
*emitted_sequence += 1;
let mut frame = Frame::new(
MemoryDomain::System(SystemSlice::from_boxed(
emitted.payload.into_boxed_slice(),
)),
timing,
sequence,
);
frame.meta = anchor_meta.clone();
frame
})
.collect()
};
if let Some(first) = out.first_mut() {
attach_metadata(first, staged, frame_dims);
}
Ok(out)
}
Ok(false) => Ok(Vec::new()),
Err(e) => Err(py_fail(py, e)),
}
}
fn call_system(
py: Python<'_>,
instance: &Py<PyAny>,
spans: &[(*mut u8, usize)],
job: &Job,
sink: &Py<MetaSink>,
) -> PyResult<bool> {
let buffers: Vec<Py<FrameBuffer>> = spans
.iter()
.map(|&(ptr, len)| {
Py::new(
py,
FrameBuffer {
ptr,
len,
exports: core::cell::Cell::new(0),
},
)
})
.collect::<PyResult<_>>()?;
let bound = instance.bind(py);
let produced = match &job.caps {
JobCaps::Payload(caps) => {
let list = pyo3::types::PyList::new(py, buffers.iter().map(|b| b.clone_ref(py)))?;
bound.call_method1(
PAYLOAD_HOOK,
(list, caps.to_gst_string(), sink.clone_ref(py)),
)?;
true
}
JobCaps::RawVideo { width, height, fmt } => {
let (w, h, fmt) = (*width, *height, format_to_py(*fmt));
match job.kind {
JobKind::Batch => {
let list =
pyo3::types::PyList::new(py, buffers.iter().map(|b| b.clone_ref(py)))?;
bound
.call_method1("g2g_process_batch", (list, w, h, fmt, sink.clone_ref(py)))?;
true
}
JobKind::Transform => {
let buffer = buffers
.first()
.expect("single job has one frame")
.clone_ref(py);
bound.call_method1("g2g_process", (buffer, w, h, fmt, sink.clone_ref(py)))?;
true
}
JobKind::Produce => {
let buffer = buffers
.first()
.expect("produce job has one frame")
.clone_ref(py);
let ret = bound
.call_method1("g2g_produce", (buffer, w, h, fmt, sink.clone_ref(py)))?;
ret.extract::<bool>()?
}
JobKind::ProduceCuda => unreachable!("a GPU produce job carries no System frame"),
}
}
};
if buffers.iter().any(|b| b.borrow(py).exports.get() != 0) {
return Err(PyBufferError::new_err(
"g2g_process retained a frame buffer view past return (use-after-free risk)",
));
}
Ok(produced)
}
fn cuda_geometry(job: &Job) -> PyResult<(u32, u32, RawVideoFormat)> {
job.caps
.video()
.ok_or_else(|| PyRuntimeError::new_err("a GPU job needs raw-video caps"))
}
fn call_cuda(
py: Python<'_>,
instance: &Py<PyAny>,
luma: CudaPlane,
chroma: CudaPlane,
job: &Job,
sink: &Py<MetaSink>,
) -> PyResult<bool> {
let planes = [Py::new(py, luma)?, Py::new(py, chroma)?];
let (w, h, _) = cuda_geometry(job)?;
instance.bind(py).call_method1(
CUDA_HOOK,
(
planes[0].clone_ref(py),
planes[1].clone_ref(py),
w,
h,
sink.clone_ref(py),
),
)?;
planes_released(py, &planes)
}
fn call_cuda_batch(
py: Python<'_>,
instance: &Py<PyAny>,
planes: Vec<(CudaPlane, CudaPlane)>,
job: &Job,
sink: &Py<MetaSink>,
) -> PyResult<bool> {
let mut handles = Vec::with_capacity(planes.len() * 2);
let mut pairs = Vec::with_capacity(planes.len());
for (luma, chroma) in planes {
let (luma, chroma) = (Py::new(py, luma)?, Py::new(py, chroma)?);
pairs.push((luma.clone_ref(py), chroma.clone_ref(py)));
handles.push(luma);
handles.push(chroma);
}
let (width, height, _) = cuda_geometry(job)?;
let list = pyo3::types::PyList::new(py, pairs)?;
instance.bind(py).call_method1(
CUDA_BATCH_HOOK,
(list.clone(), width, height, sink.clone_ref(py)),
)?;
drop(list);
planes_released(py, &handles)
}
fn call_produce_cuda(
py: Python<'_>,
instance: &Py<PyAny>,
job: &mut Job,
sink: &Py<MetaSink>,
) -> PyResult<bool> {
let (width, height, fmt) = cuda_geometry(job)?;
let bound = instance.bind(py);
let returned = bound.call_method1(CUDA_PRODUCE_HOOK, (width, height, sink.clone_ref(py)))?;
if !returned.is_truthy()? {
return Ok(false);
}
let (luma, chroma): (Bound<'_, PyAny>, Bound<'_, PyAny>) = returned.extract()?;
let context = reported_cuda_context(bound)?;
let device_ordinal = reported_cuda_device(bound)?;
let buffer = produced_cuda_buffer(&luma, &chroma, fmt, width, height, context, device_ordinal)?;
job.frames.push(Frame {
domain: MemoryDomain::Cuda(buffer),
timing: g2g_core::FrameTiming::default(),
sequence: 0,
meta: Default::default(),
});
Ok(true)
}
fn reported_cuda_context(instance: &Bound<'_, PyAny>) -> PyResult<u64> {
match instance.getattr(CUDA_CONTEXT_ATTR) {
Ok(value) if !value.is_none() => value.extract(),
_ => Ok(0),
}
}
fn reported_cuda_device(instance: &Bound<'_, PyAny>) -> PyResult<i32> {
match instance.getattr(CUDA_DEVICE_ATTR) {
Ok(value) if !value.is_none() => value.extract(),
_ => Ok(0),
}
}
fn planes_released(py: Python<'_>, planes: &[Py<CudaPlane>]) -> PyResult<bool> {
if planes.iter().any(|plane| plane.get_refcnt(py) > 1) {
return Err(PyRuntimeError::new_err(
"a hosted element retained a CudaPlane past return (use-after-free risk)",
));
}
Ok(true)
}
#[cfg(feature = "analytics")]
fn attach_metadata(frame: &mut Frame, staged: Vec<Staged>, frame_dims: Option<(u32, u32)>) {
use g2g_core::{
AnalyticsMeta, AnalyticsNode, BBox, BlobMeta, Classification, ObjectDetection,
RelationKind, Tracking,
};
if staged.is_empty() {
return;
}
let (sx, sy) = match frame_dims {
Some((w, h)) => (1.0 / w as f32, 1.0 / h as f32),
None => (0.0, 0.0),
};
let mut analytics = AnalyticsMeta::new();
let mut blobs = BlobMeta::new();
let mut node_of_staged: Vec<Option<usize>> = vec![None; staged.len()];
let mut relations: Vec<(usize, usize)> = Vec::new();
for (index, s) in staged.into_iter().enumerate() {
match s {
Staged::Object {
label,
x,
y,
w,
h,
score,
} => {
if frame_dims.is_none() {
g2g_warn!(
Target::category("pyelement"),
"hosted element staged a detection on a stream with no \
pixels, dropping it"
);
continue;
}
node_of_staged[index] = Some(analytics.add_detection(ObjectDetection {
bbox: BBox {
x: x * sx,
y: y * sy,
w: w * sx,
h: h * sy,
},
label,
confidence: score,
}));
}
Staged::Classification { label, score } => {
node_of_staged[index] = Some(analytics.push(AnalyticsNode::Classification(
Classification {
label,
confidence: score,
},
)));
}
Staged::Tracking { object_id } => {
node_of_staged[index] =
Some(analytics.push(AnalyticsNode::Tracking(Tracking { object_id })));
}
Staged::Relation { from, to } => relations.push((from, to)),
Staged::ClassNames { names } => analytics.set_class_names(names),
Staged::Blob { header, payload } => blobs.push(header, payload),
}
}
for (from, to) in relations {
let (Some(Some(from)), Some(Some(to))) = (
node_of_staged.get(from).copied(),
node_of_staged.get(to).copied(),
) else {
continue;
};
analytics.relate(from, to, RelationKind::Tracks);
}
if !analytics.nodes.is_empty() {
frame.meta.attach(analytics);
}
if !blobs.is_empty() {
frame.meta.attach(blobs);
}
}
#[cfg(not(feature = "analytics"))]
fn attach_metadata(_frame: &mut Frame, _staged: Vec<Staged>, _frame_dims: Option<(u32, u32)>) {}
fn job_caps(caps: &Caps) -> Result<JobCaps, G2gError> {
match caps {
Caps::RawVideo { .. } => raw_video_caps(caps),
other => Ok(JobCaps::Payload(other.clone())),
}
}
fn raw_video_caps(caps: &Caps) -> Result<JobCaps, G2gError> {
match caps {
Caps::RawVideo {
format,
width,
height,
..
} => Ok(JobCaps::RawVideo {
width: dim_fixed(width)?,
height: dim_fixed(height)?,
fmt: *format,
}),
_ => Err(G2gError::CapsMismatch),
}
}
fn dim_fixed(d: &Dim) -> Result<u32, G2gError> {
match d {
Dim::Fixed(v) => Ok(*v),
_ => Err(G2gError::FixationFailed),
}
}
fn py_fail(py: Python<'_>, e: PyErr) -> G2gError {
e.print(py);
G2gError::Hardware(HardwareError::Other)
}