Capture PS3 Eye pixels through direct driver

This commit is contained in:
Meta Vens 2026-05-27 14:33:36 +01:00
commit 58e7af8a76
11 changed files with 593 additions and 1 deletions

View file

@ -118,6 +118,11 @@ if (args.Any(arg => string.Equals(arg, "--ks-camera-driver-smoke", StringCompari
return RunKsCameraDriverSmoke(args);
}
if (args.Any(arg => string.Equals(arg, "--ps3eye-driver-smoke", StringComparison.OrdinalIgnoreCase)))
{
return RunPs3EyeDriverSmoke(args);
}
if (args.Any(arg => string.Equals(arg, "--mimir-spectrum-upload-smoke", StringComparison.OrdinalIgnoreCase)))
{
return RunMimirSpectrumUploadSmoke();
@ -871,6 +876,57 @@ static int RunKsCameraDriverSmoke(string[] args)
return 1;
}
static int RunPs3EyeDriverSmoke(string[] args)
{
var library = ParseStringOption(args, "--native-library", Path.GetFullPath("native/camera_capture/build/Release/mimir_ps3eye_capture.dll"));
var sourceId = ParseStringOption(args, "--source-id", "ps3-eye-0");
var cameraIndex = ParseIntOption(args, "--camera-index", 0);
var width = ParseIntOption(args, "--width", 320);
var height = ParseIntOption(args, "--height", 240);
var fps = ParseIntOption(args, "--fps", 187);
var timeoutMs = ParseIntOption(args, "--timeout-ms", 3000);
var broker = new FakeFieldResourceBroker();
using var driver = new MimirPs3EyeVideoCaptureDriver(new MimirPs3EyeVideoCaptureDriverOptions(
library,
sourceId,
cameraIndex,
width,
height,
fps));
using var source = new MimirVideoCaptureDriverSource(
new MimirStreamDescriptor(sourceId, MimirStreamKind.Video, MimirStreamOrigin.LocalDevice),
driver,
static () => StopwatchTicksToNs(Stopwatch.GetTimestamp()));
source.AttachTextureLeaseClient(new MimirFensalirTextureLeaseClient(broker));
var deadline = Stopwatch.GetTimestamp() + (long)(timeoutMs / 1000.0 * Stopwatch.Frequency);
while (Stopwatch.GetTimestamp() < deadline)
{
if (!source.TryRead(out var sample))
{
Thread.Sleep(1);
continue;
}
var frame = sample.VideoFrame;
Console.WriteLine(
$"ps3eye-driver-smoke source={sample.SourceId} camera={cameraIndex} {frame?.Width}x{frame?.Height} format={frame?.PixelFormat} liveBytes={sample.ByteLength} uploadedBytes={source.LastUploadedByteLength} uploaded={broker.UploadedResourceKey} copies={source.LastUploadedCopyCount}");
return frame != null &&
frame.Width == width &&
frame.Height == height &&
frame.PixelFormat == MimirVideoPixelFormat.Bayer8 &&
source.LastUploadedByteLength == width * height &&
source.LastUploadedCopyCount == 1 &&
broker.UploadedResourceKey == MimirFensalirTextureLeaseClient.ResourceKeyForSource(sourceId)
? 0
: 1;
}
Console.WriteLine($"ps3eye-driver-smoke timeout source={sourceId} camera={cameraIndex} {width}x{height}@{fps}");
return 1;
}
static long StopwatchTicksToNs(long ticks)
{
return checked((long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency)));

View file

@ -0,0 +1,172 @@
using System.Runtime.InteropServices;
namespace Mimir.Runtime.Synchronization;
public sealed record MimirPs3EyeVideoCaptureDriverOptions(
string NativeLibraryPath,
string SourceId,
int CameraIndex,
int Width,
int Height,
int FramesPerSecond);
public sealed unsafe class MimirPs3EyeVideoCaptureDriver : IMimirVideoCaptureDriver, IMimirFensalirTextureLeaseReceiver
{
private readonly Native native;
private readonly nint capture;
private readonly byte[] scratch;
private readonly string sourceId;
private MimirFensalirTextureLeaseClient? textureLeaseClient;
private string resourceKey = "";
private bool disposed;
public MimirPs3EyeVideoCaptureDriver(MimirPs3EyeVideoCaptureDriverOptions options)
{
if (string.IsNullOrWhiteSpace(options.NativeLibraryPath))
{
throw new ArgumentException("PS3 Eye capture requires a native library path.", nameof(options));
}
if (string.IsNullOrWhiteSpace(options.SourceId))
{
throw new ArgumentException("PS3 Eye capture requires a source id.", nameof(options));
}
native = Native.Load(options.NativeLibraryPath);
sourceId = options.SourceId;
capture = native.Create(options.CameraIndex, options.Width, options.Height, options.FramesPerSecond);
if (capture == 0)
{
throw new InvalidOperationException($"PS3 Eye capture could not open camera {options.CameraIndex} at {options.Width}x{options.Height}@{options.FramesPerSecond}.");
}
scratch = new byte[checked(options.Width * options.Height)];
if (!native.Start(capture))
{
throw new InvalidOperationException("PS3 Eye capture could not start.");
}
}
public string DriverName => "ps3eye-winusb-direct";
public void AttachTextureLeaseClient(MimirFensalirTextureLeaseClient? client)
{
textureLeaseClient = client;
resourceKey = "";
}
public bool TryCapture(out MimirVideoFrameDescriptor frame, out ReadOnlyMemory<byte> data)
{
fixed (byte* destination = scratch)
{
var byteLength = native.Read(
capture,
out var width,
out var height,
out var stride,
out var timestampNs,
out var sequence,
destination,
scratch.Length);
if (byteLength <= 0)
{
frame = default!;
data = default;
return false;
}
EnsureLease(width, height, stride, timestampNs, sequence);
frame = new MimirVideoFrameDescriptor(
width,
height,
MimirVideoPixelFormat.Bayer8,
stride,
timestampNs,
ResourceKey: resourceKey,
ProducerFenceValue: sequence);
data = scratch.AsMemory(0, byteLength).ToArray();
return true;
}
}
public void Dispose()
{
if (disposed)
{
return;
}
disposed = true;
native.Destroy(capture);
native.Dispose();
}
private void EnsureLease(int width, int height, int stride, long timestampNs, ulong version)
{
if (!string.IsNullOrWhiteSpace(resourceKey) || textureLeaseClient == null)
{
return;
}
if (textureLeaseClient.TryLeaseTexture2D(new MimirFensalirTextureLeaseRequest(
sourceId,
width,
height,
MimirVideoPixelFormat.Bayer8,
stride,
timestampNs,
version), out var lease))
{
resourceKey = lease.Frame.ResourceKey;
}
}
private sealed class Native : IDisposable
{
private readonly nint library;
private readonly delegate* unmanaged[Stdcall]<int, int, int, int, nint> create;
private readonly delegate* unmanaged[Stdcall]<nint, int> start;
private readonly delegate* unmanaged[Stdcall]<nint, out int, out int, out int, out long, out ulong, byte*, int, int> read;
private readonly delegate* unmanaged[Stdcall]<nint, void> destroy;
private Native(nint library)
{
this.library = library;
create = (delegate* unmanaged[Stdcall]<int, int, int, int, nint>)NativeLibrary.GetExport(library, "mimir_ps3eye_create");
start = (delegate* unmanaged[Stdcall]<nint, int>)NativeLibrary.GetExport(library, "mimir_ps3eye_start");
read = (delegate* unmanaged[Stdcall]<nint, out int, out int, out int, out long, out ulong, byte*, int, int>)NativeLibrary.GetExport(library, "mimir_ps3eye_read");
destroy = (delegate* unmanaged[Stdcall]<nint, void>)NativeLibrary.GetExport(library, "mimir_ps3eye_destroy");
}
public static Native Load(string path) => new(NativeLibrary.Load(path));
public nint Create(int cameraIndex, int width, int height, int framesPerSecond) =>
create(cameraIndex, width, height, framesPerSecond);
public bool Start(nint capture) => start(capture) != 0;
public int Read(
nint capture,
out int width,
out int height,
out int stride,
out long timestampNs,
out ulong sequence,
byte* destination,
int destinationBytes) =>
read(capture, out width, out height, out stride, out timestampNs, out sequence, destination, destinationBytes);
public void Destroy(nint capture)
{
if (capture != 0)
{
destroy(capture);
}
}
public void Dispose()
{
NativeLibrary.Free(library);
}
}
}

View file

@ -172,6 +172,26 @@ public sealed class MimirRuntimeConfiguration
static () => StopwatchTicksToNs(System.Diagnostics.Stopwatch.GetTimestamp())));
}
if (string.Equals(stream.Adapter, "ps3eye", StringComparison.OrdinalIgnoreCase)
|| string.Equals(stream.Adapter, "ps3eye-direct", StringComparison.OrdinalIgnoreCase))
{
if (!OperatingSystem.IsWindows())
{
throw new PlatformNotSupportedException("The Mimir PS3 Eye source requires Windows.");
}
return new MimirStreamSourceFactory(descriptor, () => new MimirVideoCaptureDriverSource(
descriptor,
new MimirPs3EyeVideoCaptureDriver(new MimirPs3EyeVideoCaptureDriverOptions(
ResolveCommand(stream.Command, configDirectory),
stream.SourceId,
stream.CameraIndex,
stream.Width,
stream.Height,
stream.FramesPerSecond)),
static () => StopwatchTicksToNs(System.Diagnostics.Stopwatch.GetTimestamp())));
}
if (string.Equals(stream.Adapter, "frame-events", StringComparison.OrdinalIgnoreCase)
|| string.Equals(stream.Adapter, "json-lines", StringComparison.OrdinalIgnoreCase))
{
@ -352,6 +372,10 @@ public sealed class MimirStreamConfig
public int QueueDepth { get; set; } = 8;
public int CameraIndex { get; set; }
public int FramesPerSecond { get; set; }
public string DisplayNameForSource(string sourceId)
{
if (SourceLabels.TryGetValue(sourceId, out var label) && !string.IsNullOrWhiteSpace(label))