An app built with `--remote` now serves a localhost HTTP control surface:
window list, per-window PNG grabs, real mouse/key/text injection, widget
rects, a log ring buffer, and `/gq` (grab every window, then quit). It exists
so a test or an agent can DRIVE a running app instead of reasoning about it
from source — the protocol is documented in AGENTS.md. Grabs are targeted per
window (`/g?w=N`), so a multi-window app is captured window by window rather
than whichever pass happens to present first, and `log!` mirrors into the ring
buffer without anyone owning the app's stdout.
platform/video grows a streaming half beside the file half. StreamEncoder /
StreamDecoder with Apple VideoToolbox and Windows Media Foundation backends,
Annex-B framing, and all-intra bound through pEncodingParameters on Windows —
the only control that MFT actually honors, as the readbacks claim success for
everything else. The file decoder can now be asked for a SPECIFIC frame rather
than only the next one, which is what frame-exact seek and bounce playback
need. Tests cover file seek and the stream round trip.
Draw shaders gain `Rgba16F` and `Rgba32F` color formats to pair with the
float render textures: blending off, whole-texel writes, meant for GPU
simulation state (particle position/velocity, fluid fields) rather than
pictures.
Windowing and dialogs:
- `CxOsOp::SetChromelessWhenMaximized` drops the native maximized border
strip on Windows, so a maximized window reads as a clean picture.
- `Cx::open_select_folder_dialog` opens the native folder picker with a
title and start location, answered by a `FileDialogAction` in the actions
pass; cancelling is a first-class outcome, not an error.
- Windows reports a user close the way macos.rs already did.
- macOS swaps the titlebar container so WindowDragQuery alone decides window
drags, and the delegates carry a panic shield.
- `Windows::id_iter()` enumerates window slots generation-correctly.
Headless: the virtual GPU and its rasterizer are substantially rebuilt around
the shader runtime preamble, making `MAKEPAD=headless` render-to-PNG a real
test surface rather than a smoke check. `PerfMonitor::frames_painted()` lets a
scripted driver pace itself to PRESENTED frames instead of queueing passes
faster than the GPU retires them.
201 lines
9.6 KiB
Rust
201 lines
9.6 KiB
Rust
//! Windows backend for the low-latency stream decoder: the Microsoft
|
|
//! H.264 decoder MFT (`CLSID_CMSH264DecoderMFT`), driven directly via
|
|
//! `IMFTransform::ProcessInput`/`ProcessOutput` (see [`crate::windows_
|
|
//! mft`]).
|
|
//!
|
|
//! **UNVERIFIED — cross-compiled only, never run.** See `windows_stream_
|
|
//! encoder`'s module doc for the full explanation (no Windows machine
|
|
//! available; several constants below are hand-derived because the vendored
|
|
//! `windows-rs` binding subset in this repo does not include them).
|
|
//!
|
|
//! No DXVA/hardware surface path (the user-facing spec calls this
|
|
//! "optional, later") — output is read back to system memory NV12 via
|
|
//! `IMFMediaBuffer::Lock`, same as the file-based decoder.
|
|
//!
|
|
//! The input type is set to bare H.264 (no frame-size hint — the decoder
|
|
//! determines the real picture size from the bitstream's SPS). The first
|
|
//! `ProcessOutput` calls are expected to return `MF_E_TRANSFORM_STREAM_
|
|
//! CHANGE` once the decoder has parsed enough of the stream to know its
|
|
//! output format; `renegotiate_output_type` handles that by walking `Get
|
|
//! OutputAvailableType` for an NV12 entry and calling `SetOutputType`.
|
|
|
|
use crate::stream_decoder::DecodedFrame;
|
|
use crate::stream_encoder::StreamVideoCodec;
|
|
use crate::windows_encoder::{ensure_media_foundation, hr_err};
|
|
use crate::windows_mft::{
|
|
self, MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, MFT_MESSAGE_NOTIFY_START_OF_STREAM, MFT_OUTPUT_STREAM_PROVIDES_SAMPLES,
|
|
MF_E_TRANSFORM_NEED_MORE_INPUT, MF_E_TRANSFORM_STREAM_CHANGE,
|
|
};
|
|
use crate::VideoFileError;
|
|
use std::collections::VecDeque;
|
|
use windows::{
|
|
core::GUID,
|
|
Win32::Media::MediaFoundation::{
|
|
IMFSample, IMFTransform, MFCreateMediaType, MFCreateMemoryBuffer, MFCreateSample, MFMediaType_Video,
|
|
MFVideoFormat_H264, MFVideoFormat_NV12, MF_MT_FRAME_SIZE, MF_MT_MAJOR_TYPE, MF_MT_SUBTYPE,
|
|
},
|
|
Win32::System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER},
|
|
};
|
|
|
|
/// Well-known Microsoft H.264 decoder MFT CLSID (msmpeg2vdec.dll) — stable
|
|
/// and documented since Windows 7; not in the vendored bindings.
|
|
const CLSID_CMS_H264_DECODER_MFT: GUID = GUID::from_u128(0x62ce7e72_4c71_4d20_b15d_452831a87d9d);
|
|
|
|
fn create_transform() -> Result<IMFTransform, VideoFileError> {
|
|
ensure_media_foundation()?;
|
|
unsafe { CoCreateInstance(&CLSID_CMS_H264_DECODER_MFT, None, CLSCTX_INPROC_SERVER) }
|
|
.map_err(|e| hr_err("CoCreateInstance(CLSID_CMSH264DecoderMFT)", e))
|
|
}
|
|
|
|
fn make_input_sample(annex_b: &[u8], pts_100ns: i64) -> Result<IMFSample, VideoFileError> {
|
|
unsafe {
|
|
let buffer = MFCreateMemoryBuffer(annex_b.len().max(1) as u32).map_err(|e| hr_err("MFCreateMemoryBuffer", e))?;
|
|
let mut ptr = std::ptr::null_mut();
|
|
buffer.Lock(&mut ptr, None, None).map_err(|e| hr_err("Lock", e))?;
|
|
std::ptr::copy_nonoverlapping(annex_b.as_ptr(), ptr, annex_b.len());
|
|
buffer.Unlock().map_err(|e| hr_err("Unlock", e))?;
|
|
buffer.SetCurrentLength(annex_b.len() as u32).map_err(|e| hr_err("SetCurrentLength", e))?;
|
|
let sample = MFCreateSample().map_err(|e| hr_err("MFCreateSample", e))?;
|
|
sample.AddBuffer(&buffer).map_err(|e| hr_err("AddBuffer", e))?;
|
|
sample.SetSampleTime(pts_100ns).map_err(|e| hr_err("SetSampleTime", e))?;
|
|
Ok(sample)
|
|
}
|
|
}
|
|
|
|
fn make_output_sample(size: u32) -> Result<IMFSample, VideoFileError> {
|
|
unsafe {
|
|
let buffer = MFCreateMemoryBuffer(size.max(1)).map_err(|e| hr_err("MFCreateMemoryBuffer(out)", e))?;
|
|
let sample = MFCreateSample().map_err(|e| hr_err("MFCreateSample(out)", e))?;
|
|
sample.AddBuffer(&buffer).map_err(|e| hr_err("AddBuffer(out)", e))?;
|
|
Ok(sample)
|
|
}
|
|
}
|
|
|
|
pub struct WindowsStreamDecoder {
|
|
transform: IMFTransform,
|
|
output_negotiated: bool,
|
|
provides_samples: bool,
|
|
output_buffer_size: u32,
|
|
width: u32,
|
|
height: u32,
|
|
pending_pts: VecDeque<i64>,
|
|
}
|
|
|
|
impl WindowsStreamDecoder {
|
|
pub fn new(codec: StreamVideoCodec) -> Result<Self, VideoFileError> {
|
|
if !matches!(codec, StreamVideoCodec::H264) {
|
|
return Err(VideoFileError::new("windows stream decoder: only H264 is implemented"));
|
|
}
|
|
let transform = create_transform()?;
|
|
unsafe {
|
|
let in_type = MFCreateMediaType().map_err(|e| hr_err("MFCreateMediaType(in)", e))?;
|
|
in_type.SetGUID(&MF_MT_MAJOR_TYPE, &MFMediaType_Video).map_err(|e| hr_err("set in major type", e))?;
|
|
in_type.SetGUID(&MF_MT_SUBTYPE, &MFVideoFormat_H264).map_err(|e| hr_err("set in subtype", e))?;
|
|
windows_mft::set_input_type(&transform, &in_type).map_err(|e| hr_err("IMFTransform::SetInputType", e))?;
|
|
windows_mft::process_message(&transform, MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, 0)
|
|
.map_err(|e| hr_err("ProcessMessage(BEGIN_STREAMING)", e))?;
|
|
windows_mft::process_message(&transform, MFT_MESSAGE_NOTIFY_START_OF_STREAM, 0)
|
|
.map_err(|e| hr_err("ProcessMessage(START_OF_STREAM)", e))?;
|
|
}
|
|
Ok(Self {
|
|
transform,
|
|
output_negotiated: false,
|
|
provides_samples: false,
|
|
output_buffer_size: 0,
|
|
width: 0,
|
|
height: 0,
|
|
pending_pts: VecDeque::new(),
|
|
})
|
|
}
|
|
|
|
/// Walks `GetOutputAvailableType` looking for an NV12 entry (the decoder
|
|
/// only offers real entries once it has parsed enough of the bitstream —
|
|
/// this is called in response to `MF_E_TRANSFORM_STREAM_CHANGE`) and
|
|
/// commits it with `SetOutputType`.
|
|
fn renegotiate_output_type(&mut self) -> Result<(), VideoFileError> {
|
|
unsafe {
|
|
let mut chosen = None;
|
|
for index in 0..16u32 {
|
|
let Ok(candidate) = windows_mft::get_output_available_type(&self.transform, index) else {
|
|
break;
|
|
};
|
|
if let Ok(subtype) = candidate.GetGUID(&MF_MT_SUBTYPE) {
|
|
if subtype == MFVideoFormat_NV12 {
|
|
chosen = Some(candidate);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
let Some(chosen_type) = chosen else {
|
|
return Err(VideoFileError::new(
|
|
"windows stream decoder: no NV12 output type offered by CMSH264DecoderMFT",
|
|
));
|
|
};
|
|
let packed = chosen_type.GetUINT64(&MF_MT_FRAME_SIZE).map_err(|e| hr_err("GetUINT64(FRAME_SIZE)", e))?;
|
|
self.width = (packed >> 32) as u32;
|
|
self.height = (packed & 0xFFFF_FFFF) as u32;
|
|
windows_mft::set_output_type(&self.transform, &chosen_type).map_err(|e| hr_err("SetOutputType(NV12)", e))?;
|
|
let stream_info =
|
|
windows_mft::get_output_stream_info(&self.transform).map_err(|e| hr_err("GetOutputStreamInfo", e))?;
|
|
self.provides_samples = stream_info.dwFlags & MFT_OUTPUT_STREAM_PROVIDES_SAMPLES != 0;
|
|
self.output_buffer_size = stream_info.cbSize;
|
|
self.output_negotiated = true;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn push_packet(&mut self, annex_b_data: &[u8], pts_100ns: i64) -> Result<Vec<DecodedFrame>, VideoFileError> {
|
|
// See `WindowsStreamEncoder::push_frame_nv12`'s identical comment —
|
|
// this type is `Send` and may be called from a different thread
|
|
// than the one that constructed it; re-assert MTA membership here.
|
|
ensure_media_foundation()?;
|
|
let sample = make_input_sample(annex_b_data, pts_100ns)?;
|
|
let hr = unsafe { windows_mft::process_input(&self.transform, &sample) };
|
|
if hr.is_err() {
|
|
return Err(VideoFileError::with_code("IMFTransform::ProcessInput", hr.0));
|
|
}
|
|
self.pending_pts.push_back(pts_100ns);
|
|
self.drain_available()
|
|
}
|
|
|
|
fn drain_available(&mut self) -> Result<Vec<DecodedFrame>, VideoFileError> {
|
|
let mut frames = Vec::new();
|
|
loop {
|
|
if !self.output_negotiated {
|
|
// Nothing to pull yet — the decoder hasn't told us its
|
|
// output format (needs SPS/PPS + at least one VCL NAL).
|
|
break;
|
|
}
|
|
let provided = if self.provides_samples { None } else { Some(make_output_sample(self.output_buffer_size)?) };
|
|
let (hr, sample) = unsafe { windows_mft::process_output(&self.transform, provided) };
|
|
if hr.0 == MF_E_TRANSFORM_NEED_MORE_INPUT {
|
|
break;
|
|
}
|
|
if hr.0 == MF_E_TRANSFORM_STREAM_CHANGE {
|
|
self.renegotiate_output_type()?;
|
|
continue;
|
|
}
|
|
if hr.is_err() {
|
|
return Err(VideoFileError::with_code("IMFTransform::ProcessOutput", hr.0));
|
|
}
|
|
let Some(sample) = sample else { break };
|
|
let buffer = unsafe { sample.ConvertToContiguousBuffer() }.map_err(|e| hr_err("ConvertToContiguousBuffer", e))?;
|
|
let (mut ptr, mut len) = (std::ptr::null_mut(), 0u32);
|
|
unsafe { buffer.Lock(&mut ptr, None, Some(&mut len)) }.map_err(|e| hr_err("Lock(out)", e))?;
|
|
let nv12 = unsafe { std::slice::from_raw_parts(ptr, len as usize) }.to_vec();
|
|
unsafe { buffer.Unlock() }.map_err(|e| hr_err("Unlock(out)", e))?;
|
|
let pts_100ns = self.pending_pts.pop_front().unwrap_or(0);
|
|
frames.push(DecodedFrame { width: self.width, height: self.height, nv12, pts_100ns });
|
|
}
|
|
Ok(frames)
|
|
}
|
|
|
|
pub fn flush(&mut self) -> Result<Vec<DecodedFrame>, VideoFileError> {
|
|
self.drain_available()
|
|
}
|
|
}
|
|
|
|
// SAFETY (UNVERIFIED): see `WindowsStreamEncoder`'s identical `Send` impl
|
|
// in windows_stream_encoder.rs — same reasoning (Mutex-enforced exclusive
|
|
// access + per-thread MTA re-assertion on every entry point).
|
|
unsafe impl Send for WindowsStreamDecoder {}
|