use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use oxideav_core::{
CodecCapabilities, CodecId, CodecInfo, CodecParameters, CodecTag, Decoder, Error, Frame,
Packet, PixelFormat, Result, RuntimeContext, VideoFrame, VideoPlane,
};
use crate::win32::vfw32::{Bih, ICDECOMPRESS_NOTKEYFRAME};
use super::probe::{fourcc_to_bytes, Kind};
#[derive(Debug, Clone)]
pub struct DiscoveryRecord {
pub dll_path: PathBuf,
pub fourcc: String,
pub kind: Kind,
pub clsid: Option<String>,
}
fn record_table() -> &'static Mutex<HashMap<String, DiscoveryRecord>> {
static TABLE: OnceLock<Mutex<HashMap<String, DiscoveryRecord>>> = OnceLock::new();
TABLE.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn register_factory_for_id(codec_id: &str, record: DiscoveryRecord) {
if let Ok(mut t) = record_table().lock() {
t.insert(codec_id.to_string(), record);
}
}
pub fn lookup_record(codec_id: &str) -> Option<DiscoveryRecord> {
let t = record_table().lock().ok()?;
t.get(codec_id).cloned()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CodecAllocatorNegotiation {
pub get_allocator_hr: u32,
pub codec_allocator: u32,
pub set_properties_hr: u32,
pub commit_hr: u32,
pub using_codec_allocator: bool,
}
fn negotiation_table() -> &'static std::sync::Mutex<HashMap<String, CodecAllocatorNegotiation>> {
static T: std::sync::OnceLock<std::sync::Mutex<HashMap<String, CodecAllocatorNegotiation>>> =
std::sync::OnceLock::new();
T.get_or_init(|| std::sync::Mutex::new(HashMap::new()))
}
fn record_codec_allocator_negotiation(codec_id: &str, neg: CodecAllocatorNegotiation) {
if let Ok(mut t) = negotiation_table().lock() {
t.insert(codec_id.to_string(), neg);
}
}
pub fn last_codec_allocator_negotiation(codec_id: &str) -> Option<CodecAllocatorNegotiation> {
negotiation_table().lock().ok()?.get(codec_id).copied()
}
pub fn codec_id_for(dll_path: &Path, fourcc: &str) -> String {
let stem = dll_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
let mut id = String::with_capacity(8 + 4 + 1 + stem.len());
id.push_str("vfw_");
push_sanitised(&mut id, fourcc);
id.push('_');
push_sanitised(&mut id, stem);
id
}
fn push_sanitised(out: &mut String, s: &str) {
for c in s.chars() {
if c.is_ascii_alphanumeric() {
out.push(c.to_ascii_lowercase());
} else {
out.push('_');
}
}
}
pub fn register_codec_info(ctx: &mut RuntimeContext, codec_id: &str, fourcc: &str) {
let id = CodecId::new(codec_id.to_string());
let caps = CodecCapabilities::video("vfw_sandboxed")
.with_decode()
.with_lossy(true)
.with_priority(200);
let mut info = CodecInfo::new(id).capabilities(caps).decoder(make_decoder);
if let Some(bytes) = fourcc_to_bytes(fourcc) {
info = info.tag(CodecTag::fourcc(&bytes));
}
ctx.codecs.register(info);
}
pub fn make_decoder(params: &CodecParameters) -> Result<Box<dyn Decoder>> {
let id_str = params.codec_id.as_str();
let record = lookup_record(id_str).ok_or_else(|| {
Error::other(format!(
"vfw discovery: codec id {id_str:?} not registered (call \
oxideav_vfw::register first, or ensure OXIDEAV_VFW_CODEC_PATH \
points at a codec directory)"
))
})?;
match record.kind {
Kind::Vfw => Ok(Box::new(SandboxedVfwDecoder::new(record, params.clone())?)),
Kind::DirectShow => Ok(Box::new(SandboxedDshowDecoder::new(
record,
params.clone(),
)?)),
Kind::Unsupported => Err(Error::unsupported(
"vfw discovery: this codec was probed but found unsupported",
)),
}
}
struct SandboxedVfwDecoder {
codec_id: CodecId,
record: DiscoveryRecord,
sandbox: Option<crate::Sandbox>,
image: Option<crate::pe::Image>,
hic: u32,
begin_done: bool,
width: u32,
height: u32,
dims_from_params: bool,
fourcc_bytes: [u8; 4],
pending: Option<Packet>,
eof: bool,
}
impl SandboxedVfwDecoder {
fn new(record: DiscoveryRecord, params: CodecParameters) -> Result<Self> {
let dims_from_params = params.width.is_some() && params.height.is_some();
let width = params.width.unwrap_or(0);
let height = params.height.unwrap_or(0);
let fourcc_bytes = fourcc_to_bytes(&record.fourcc).ok_or_else(|| {
Error::other(format!(
"vfw discovery: bad fourcc {:?} in record",
record.fourcc
))
})?;
Ok(SandboxedVfwDecoder {
codec_id: params.codec_id.clone(),
record,
sandbox: None,
image: None,
hic: 0,
begin_done: false,
width,
height,
dims_from_params,
fourcc_bytes,
pending: None,
eof: false,
})
}
fn build_input_bih(&self, size_image: u32) -> Bih {
Bih {
bi_size: 40,
width: self.width as i32,
height: self.height as i32,
planes: 1,
bit_count: 24,
compression: self.fourcc_bytes,
size_image,
x_pels_per_meter: 0,
y_pels_per_meter: 0,
clr_used: 0,
clr_important: 0,
}
}
fn build_output_bih(&self) -> Bih {
Bih {
bi_size: 40,
width: self.width as i32,
height: self.height as i32,
planes: 1,
bit_count: 24,
compression: [0; 4], size_image: self.output_capacity(),
x_pels_per_meter: 0,
y_pels_per_meter: 0,
clr_used: 0,
clr_important: 0,
}
}
fn output_capacity(&self) -> u32 {
self.width.saturating_mul(self.height).saturating_mul(3)
}
fn ensure_open(&mut self) -> Result<()> {
if self.begin_done {
return Ok(());
}
if self.sandbox.is_none() {
let bytes = std::fs::read(&self.record.dll_path)
.map_err(|e| Error::other(format!("vfw discovery: read DLL failed: {e}")))?;
let mut sb = crate::Sandbox::new();
sb.cpu.set_instr_limit(8_000_000_000);
let img = sb
.load("codec.dll", &bytes)
.map_err(|e| Error::other(format!("vfw discovery: Sandbox::load failed: {e}")))?;
sb.install_codec(&img)
.map_err(|e| Error::other(format!("vfw discovery: install_codec failed: {e}")))?;
let _ = sb.call_dll_main(&img, crate::DLL_PROCESS_ATTACH);
let fcc_handler = u32::from_le_bytes(self.fourcc_bytes);
let fcc_type = u32::from_le_bytes(*b"VIDC");
let hic = sb
.ic_open(fcc_type, fcc_handler, 2)
.map_err(|e| Error::other(format!("vfw discovery: ic_open failed: {e}")))?;
if hic == 0 {
return Err(Error::other(
"vfw discovery: ICOpen returned NULL (codec rejected handler FourCC)",
));
}
self.sandbox = Some(sb);
self.image = Some(img);
self.hic = hic;
}
if !self.dims_from_params {
let probe_in = self.build_input_bih(0);
let hic = self.hic;
let sb = self
.sandbox
.as_mut()
.expect("sandbox just constructed above");
match sb.ic_decompress_get_format(hic, &probe_in) {
Ok((rc, out)) if rc == 0 && out.width > 0 && out.height > 0 => {
self.width = out.width.unsigned_abs();
self.height = out.height.unsigned_abs();
}
Ok((rc, out)) => {
return Err(Error::invalid(format!(
"vfw discovery: ICM_DECOMPRESS_GET_FORMAT \
could not establish dims (rc={rc:#010x}, \
out_width={}, out_height={}); pass \
CodecParameters.{{width,height}} explicitly",
out.width, out.height
)));
}
Err(e) => {
return Err(Error::invalid(format!(
"vfw discovery: ICM_DECOMPRESS_GET_FORMAT failed: {e}; \
pass CodecParameters.{{width,height}} explicitly"
)));
}
}
}
let bih_in = self.build_input_bih(0);
let bih_out = self.build_output_bih();
let hic = self.hic;
let sb = self
.sandbox
.as_mut()
.expect("sandbox just constructed above");
let q = sb
.ic_decompress_query(hic, &bih_in, Some(&bih_out))
.map_err(|e| Error::other(format!("vfw discovery: ic_decompress_query failed: {e}")))?;
if q != 0 {
return Err(Error::other(format!(
"vfw discovery: ic_decompress_query returned {q:#010x} \
(want 0 = ICERR_OK; codec rejected the input/output format)"
)));
}
let b = sb
.ic_decompress_begin(hic, &bih_in, &bih_out)
.map_err(|e| Error::other(format!("vfw discovery: ic_decompress_begin failed: {e}")))?;
if b != 0 {
return Err(Error::other(format!(
"vfw discovery: ic_decompress_begin returned {b:#010x} \
(want 0 = ICERR_OK)"
)));
}
self.begin_done = true;
Ok(())
}
}
impl Decoder for SandboxedVfwDecoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn send_packet(&mut self, packet: &Packet) -> Result<()> {
if self.pending.is_some() {
return Err(Error::other(
"vfw discovery: receive_frame must be called before sending another packet",
));
}
self.ensure_open()?;
self.pending = Some(packet.clone());
Ok(())
}
fn receive_frame(&mut self) -> Result<Frame> {
let packet = match self.pending.take() {
Some(p) => p,
None => {
return if self.eof {
Err(Error::Eof)
} else {
Err(Error::NeedMore)
};
}
};
let hic = self.hic;
let bih_in = self.build_input_bih(packet.data.len() as u32);
let bih_out = self.build_output_bih();
let cap = self.output_capacity();
let flags = if packet.flags.keyframe {
0
} else {
ICDECOMPRESS_NOTKEYFRAME
};
let sb = self.sandbox.as_mut().ok_or_else(|| {
Error::other("vfw discovery: receive_frame called without prior send_packet")
})?;
let (lr, raw) = sb
.ic_decompress(hic, flags, &bih_in, &packet.data, &bih_out, cap)
.map_err(|e| Error::other(format!("vfw discovery: ic_decompress trapped: {e}")))?;
if lr != 0 {
return Err(Error::other(format!(
"vfw discovery: ic_decompress returned {lr:#010x} (want 0 = ICERR_OK)"
)));
}
let width = self.width as usize;
let height = self.height as usize;
let stride = width * 3;
let mut data = vec![0u8; stride * height];
if raw.len() < stride * height {
return Err(Error::other(format!(
"vfw discovery: ic_decompress returned {} bytes, expected {} (stride*height)",
raw.len(),
stride * height,
)));
}
for row in 0..height {
let src_off = (height - 1 - row) * stride;
let dst_off = row * stride;
data[dst_off..dst_off + stride].copy_from_slice(&raw[src_off..src_off + stride]);
}
Ok(Frame::Video(VideoFrame {
pts: packet.pts,
planes: vec![VideoPlane { stride, data }],
}))
}
fn flush(&mut self) -> Result<()> {
self.eof = true;
Ok(())
}
}
impl Drop for SandboxedVfwDecoder {
fn drop(&mut self) {
if let Some(sb) = self.sandbox.as_mut() {
if self.hic != 0 {
if self.begin_done {
let _ = sb.ic_decompress_end(self.hic);
}
let _ = sb.ic_close(self.hic);
}
}
}
}
struct SandboxedDshowDecoder {
codec_id: CodecId,
record: DiscoveryRecord,
sandbox: Option<crate::Sandbox>,
image: Option<crate::pe::Image>,
filter: u32,
input_pin: u32,
mem_input_pin: u32,
host_graph: u32,
host_allocator: u32,
codec_allocator: u32,
using_codec_allocator: bool,
connection_done: bool,
width: u32,
height: u32,
fourcc_bytes: [u8; 4],
pending: Option<Packet>,
eof: bool,
last_get_state_hr: u32,
last_get_state_value: u32,
last_get_allocator_hr: u32,
last_codec_alloc_set_properties_hr: u32,
last_codec_alloc_commit_hr: u32,
}
impl SandboxedDshowDecoder {
fn new(record: DiscoveryRecord, params: CodecParameters) -> Result<Self> {
let fourcc_bytes = fourcc_to_bytes(&record.fourcc).ok_or_else(|| {
Error::other(format!(
"vfw discovery: bad fourcc {:?} in record",
record.fourcc
))
})?;
Ok(SandboxedDshowDecoder {
codec_id: params.codec_id.clone(),
record,
sandbox: None,
image: None,
filter: 0,
input_pin: 0,
mem_input_pin: 0,
host_graph: 0,
host_allocator: 0,
codec_allocator: 0,
using_codec_allocator: false,
connection_done: false,
width: params.width.unwrap_or(320),
height: params.height.unwrap_or(240),
fourcc_bytes,
pending: None,
eof: false,
last_get_state_hr: 0xFFFF_FFFF,
last_get_state_value: 0xFFFF_FFFF,
last_get_allocator_hr: 0xFFFF_FFFF,
last_codec_alloc_set_properties_hr: 0xFFFF_FFFF,
last_codec_alloc_commit_hr: 0xFFFF_FFFF,
})
}
#[allow(dead_code)]
fn last_get_state_hr(&self) -> u32 {
self.last_get_state_hr
}
#[allow(dead_code)]
fn last_get_state_value(&self) -> u32 {
self.last_get_state_value
}
#[allow(dead_code)]
fn codec_allocator(&self) -> u32 {
self.codec_allocator
}
#[allow(dead_code)]
fn using_codec_allocator(&self) -> bool {
self.using_codec_allocator
}
#[allow(dead_code)]
fn last_get_allocator_hr(&self) -> u32 {
self.last_get_allocator_hr
}
#[allow(dead_code)]
fn last_codec_alloc_set_properties_hr(&self) -> u32 {
self.last_codec_alloc_set_properties_hr
}
#[allow(dead_code)]
fn last_codec_alloc_commit_hr(&self) -> u32 {
self.last_codec_alloc_commit_hr
}
fn ensure_open(&mut self) -> Result<()> {
if self.connection_done {
return Ok(());
}
if self.sandbox.is_none() {
let bytes = std::fs::read(&self.record.dll_path).map_err(|e| {
Error::other(format!("vfw discovery (DShow): read DLL failed: {e}"))
})?;
let mut sb = crate::Sandbox::new();
sb.cpu.set_instr_limit(8_000_000_000);
let img = sb.load("codec.ax", &bytes).map_err(|e| {
Error::other(format!("vfw discovery (DShow): Sandbox::load failed: {e}"))
})?;
let _ = sb.call_dll_main(&img, crate::DLL_PROCESS_ATTACH);
let clsid_str = self.record.clsid.as_deref().ok_or_else(|| {
Error::unsupported(
"vfw discovery (DShow): record carries no CLSID — \
can't drive DllGetClassObject",
)
})?;
let clsid = crate::com::Guid::parse(clsid_str).map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): bad CLSID {clsid_str:?}: {e}"
))
})?;
let _factory = sb
.dll_get_class_object(&img, clsid, crate::IID_ICLASSFACTORY)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): DllGetClassObject failed: {e}"
))
})?;
let filter = sb
.co_create_instance(clsid, crate::IID_IBASEFILTER)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): co_create_instance failed: {e}"
))
})?;
if filter == 0 {
return Err(Error::other(
"vfw discovery (DShow): CreateInstance returned NULL filter",
));
}
self.sandbox = Some(sb);
self.image = Some(img);
self.filter = filter;
}
let sb = self.sandbox.as_mut().expect("sandbox just constructed");
if self.input_pin == 0 {
let pin = first_input_pin(sb, self.filter).ok_or_else(|| {
Error::other("vfw discovery (DShow): could not enumerate input pin")
})?;
self.input_pin = pin;
}
if self.host_graph == 0 {
let host_graph = sb.mint_host_filter_graph().map_err(|e| {
Error::other(format!("vfw discovery (DShow): mint host graph: {e}"))
})?;
let _ = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
self.filter,
crate::com::SLOT_BASEFILTER_JOIN_FILTER_GRAPH,
&[host_graph, 0],
);
self.host_graph = host_graph;
}
let captured = crate::com::host_iface_r31::walk_codec_input_pin_amts(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
self.input_pin,
8,
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): walk_codec_input_pin_amts: {e}"
))
})?;
if !captured.is_empty() {
log::debug!(
"vfw discovery (DShow): captured {} codec-native AMT(s); first subtype={}",
captured.len(),
captured[0].subtype
);
}
let synth_amt =
stage_am_media_type_dshow(sb, self.fourcc_bytes, self.width as i32, self.height as i32)
.map_err(|e| Error::other(format!("vfw discovery (DShow): stage AMT: {e}")))?;
let mut accepted_amt = 0u32;
let mut last_hr = 0u32;
for (i, cap) in captured.iter().enumerate() {
let host_out_pin = sb
.mint_host_output_pin_with_connection(cap.amt_addr, self.input_pin)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): mint host output pin (codec amt {i}): {e}"
))
})?;
let r = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
self.input_pin,
4, &[host_out_pin, cap.amt_addr],
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): IPin::ReceiveConnection (codec amt {i}) trapped: {e}"
))
})?;
last_hr = r;
if r == 0 {
accepted_amt = cap.amt_addr;
break;
}
}
if accepted_amt == 0 {
let host_out_pin = sb
.mint_host_output_pin_with_connection(synth_amt, self.input_pin)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): mint host output pin (synth): {e}"
))
})?;
let r = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
self.input_pin,
4, &[host_out_pin, synth_amt],
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): IPin::ReceiveConnection (synth) trapped: {e}"
))
})?;
if r != 0 {
return Err(Error::unsupported(format!(
"vfw discovery (DShow): IPin::ReceiveConnection rejected every \
candidate AMT (codec-native count={}, last codec-native HRESULT \
{last_hr:#010x}, synth HRESULT {r:#010x})",
captured.len()
)));
}
accepted_amt = synth_amt;
}
let amt = accepted_amt;
self.connection_done = true;
let mip = sb
.query_interface(self.input_pin, crate::IID_IMEMINPUTPIN)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): QI IMemInputPin failed: {e}"
))
})?;
self.mem_input_pin = mip;
let codec_alloc_pp = sb.host.arena_alloc(4).map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): codec_alloc out-slot arena: {e}"
))
})?;
sb.mmu
.write_initializer(codec_alloc_pp, &0u32.to_le_bytes())
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): codec_alloc out-slot init: {e}"
))
})?;
let r_ga = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
mip,
crate::com::SLOT_MEMINPUTPIN_GET_ALLOCATOR,
&[codec_alloc_pp],
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): IMemInputPin::GetAllocator trapped: {e}"
))
})?;
let codec_alloc = sb.mmu.load32(codec_alloc_pp).unwrap_or(0);
self.last_get_allocator_hr = r_ga;
log::debug!(
"vfw discovery (DShow): IMemInputPin::GetAllocator → \
hr={r_ga:#010x}, allocator={codec_alloc:#010x}"
);
record_codec_allocator_negotiation(
self.codec_id.as_str(),
CodecAllocatorNegotiation {
get_allocator_hr: r_ga,
codec_allocator: codec_alloc,
set_properties_hr: 0xFFFF_FFFF,
commit_hr: 0xFFFF_FFFF,
using_codec_allocator: false,
},
);
let cap = 256 * 1024;
let alloc = sb.mint_host_mem_allocator(4, cap, amt).map_err(|e| {
Error::other(format!("vfw discovery (DShow): mint host allocator: {e}"))
})?;
self.host_allocator = alloc;
if r_ga == crate::com::S_OK && codec_alloc != 0 {
let props = sb.host.arena_alloc(16).map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): codec_alloc props arena: {e}"
))
})?;
let actual = sb.host.arena_alloc(16).map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): codec_alloc actual arena: {e}"
))
})?;
let cb_buffer = self
.width
.saturating_mul(self.height)
.saturating_mul(3)
.max(256 * 1024);
for (off, val) in [(0u32, 4u32), (4, cb_buffer), (8, 1), (12, 0)] {
sb.mmu
.write_initializer(props + off, &val.to_le_bytes())
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): codec_alloc props write: {e}"
))
})?;
sb.mmu
.write_initializer(actual + off, &0u32.to_le_bytes())
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): codec_alloc actual write: {e}"
))
})?;
}
let r_sp = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
codec_alloc,
crate::com::SLOT_MEMALLOCATOR_SET_PROPERTIES,
&[props, actual],
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): codec_alloc SetProperties trapped: {e}"
))
})?;
self.last_codec_alloc_set_properties_hr = r_sp;
log::debug!(
"vfw discovery (DShow): codec_alloc SetProperties → \
hr={r_sp:#010x} (cBuffers=4 cbBuffer={cb_buffer} \
cbAlign=1 cbPrefix=0)"
);
let mut commit_ok = false;
if (r_sp & 0x8000_0000) == 0 {
let r_co = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
codec_alloc,
crate::com::SLOT_MEMALLOCATOR_COMMIT,
&[],
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): codec_alloc Commit trapped: {e}"
))
})?;
self.last_codec_alloc_commit_hr = r_co;
log::debug!("vfw discovery (DShow): codec_alloc Commit → hr={r_co:#010x}");
commit_ok = (r_co & 0x8000_0000) == 0;
}
if commit_ok {
self.codec_allocator = codec_alloc;
self.using_codec_allocator = true;
}
record_codec_allocator_negotiation(
self.codec_id.as_str(),
CodecAllocatorNegotiation {
get_allocator_hr: r_ga,
codec_allocator: codec_alloc,
set_properties_hr: self.last_codec_alloc_set_properties_hr,
commit_hr: self.last_codec_alloc_commit_hr,
using_codec_allocator: self.using_codec_allocator,
},
);
}
let advertised_alloc = if self.using_codec_allocator {
self.codec_allocator
} else {
alloc
};
let r_na = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
mip,
4, &[advertised_alloc, 0],
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): NotifyAllocator trapped: {e}"
))
})?;
log::debug!(
"vfw discovery (DShow): NotifyAllocator → {r_na:#010x}; \
alloc = {advertised_alloc:#010x} (codec={})",
self.using_codec_allocator
);
let (h_pin, h_mip) = sb.host_iface_r31_mint_input_pin_pair().map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): mint host input pin pair: {e}"
))
})?;
let _h_filter = sb.host_iface_r31_mint_base_filter(h_pin).map_err(|e| {
Error::other(format!("vfw discovery (DShow): mint host base filter: {e}"))
})?;
if let Some(out_pin) = first_output_pin_dshow(sb, self.filter, self.input_pin) {
let dn_amt = stage_am_media_type_rgb24_dshow(sb, self.width as i32, self.height as i32)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): stage downstream RGB24 AMT: {e}"
))
})?;
let r_dn = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
out_pin,
4, &[h_pin, dn_amt],
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): output ReceiveConnection trapped: {e}"
))
})?;
log::debug!(
"vfw discovery (DShow): downstream Connect → {r_dn:#010x} (out_pin={out_pin:#010x})"
);
}
let _ = h_mip;
let r_commit = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
alloc,
crate::com::SLOT_MEMALLOCATOR_COMMIT,
&[],
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): host allocator Commit trapped: {e}"
))
})?;
log::debug!("vfw discovery (DShow): host alloc Commit → {r_commit:#010x}");
let r_pause = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
self.filter,
crate::com::SLOT_MEDIAFILTER_PAUSE,
&[],
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): IMediaFilter::Pause trapped: {e}"
))
})?;
log::debug!("vfw discovery (DShow): IMediaFilter::Pause → {r_pause:#010x}");
let r_run = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
self.filter,
crate::com::SLOT_MEDIAFILTER_RUN,
&[0, 0],
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): IMediaFilter::Run trapped: {e}"
))
})?;
log::debug!("vfw discovery (DShow): IMediaFilter::Run(0) → {r_run:#010x}");
let state_slot = sb.host.arena_alloc(4).map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): IMediaFilter::GetState arena: {e}"
))
})?;
sb.mmu
.write_initializer(state_slot, &0xFFFF_FFFFu32.to_le_bytes())
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): IMediaFilter::GetState seed: {e}"
))
})?;
let r_state = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
self.filter,
crate::com::SLOT_MEDIAFILTER_GET_STATE,
&[1000, state_slot],
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): IMediaFilter::GetState trapped: {e}"
))
})?;
let state_value = sb.mmu.load32(state_slot).unwrap_or(0xFFFF_FFFF);
self.last_get_state_hr = r_state;
self.last_get_state_value = state_value;
log::debug!(
"vfw discovery (DShow): IMediaFilter::GetState(1000ms) \
→ hr={r_state:#010x}, state={state_value:#010x} \
(Stopped=0, Paused=1, Running=2)"
);
Ok(())
}
}
fn first_output_pin_dshow(sb: &mut crate::Sandbox, filter: u32, skip: u32) -> Option<u32> {
pin_with_direction(sb, filter, crate::com::PIN_DIRECTION_OUTPUT, Some(skip))
}
fn stage_am_media_type_rgb24_dshow(
sb: &mut crate::Sandbox,
width: i32,
height: i32,
) -> Result<u32> {
let to_oxide =
|e: crate::Error| Error::other(format!("vfw discovery (DShow): stage RGB24 AMT: {e}"));
let blob = sb
.host
.arena_alloc(72 + 88 + 16)
.map_err(|e| to_oxide(crate::Error::Win32(e)))?;
let amt = blob;
let fmt = blob + 72;
let mediatype_video =
crate::com::Guid::parse("{73646976-0000-0010-8000-00AA00389B71}").unwrap();
let format_videoinfo =
crate::com::Guid::parse("{05589F80-C356-11CE-BF01-00AA0055595A}").unwrap();
let mediasubtype_rgb24 =
crate::com::Guid::parse("{E436EB7D-524F-11CE-9F53-0020AF0BA770}").unwrap();
let trap = |e: crate::emulator::Trap| to_oxide(crate::Error::Trap(e));
mediatype_video.stage(&mut sb.mmu, amt).map_err(trap)?;
mediasubtype_rgb24
.stage(&mut sb.mmu, amt + 16)
.map_err(trap)?;
sb.mmu
.write_initializer(amt + 32, &1u32.to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(amt + 36, &0u32.to_le_bytes())
.map_err(trap)?;
let stride = (width.unsigned_abs() * 3 + 3) & !3;
let img = stride * height.unsigned_abs();
sb.mmu
.write_initializer(amt + 40, &img.to_le_bytes())
.map_err(trap)?;
format_videoinfo
.stage(&mut sb.mmu, amt + 44)
.map_err(trap)?;
sb.mmu
.write_initializer(amt + 60, &0u32.to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(amt + 64, &88u32.to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(amt + 68, &fmt.to_le_bytes())
.map_err(trap)?;
for i in 0..48u32 {
sb.mmu.store8(fmt + i, 0).map_err(trap)?;
}
let bih = fmt + 48;
sb.mmu
.write_initializer(bih, &40u32.to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(bih + 4, &(width as u32).to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(bih + 8, &(height as u32).to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(bih + 12, &1u16.to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(bih + 14, &24u16.to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(bih + 16, &0u32.to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(bih + 20, &img.to_le_bytes())
.map_err(trap)?;
for off in [24u32, 28, 32, 36] {
sb.mmu
.write_initializer(bih + off, &0u32.to_le_bytes())
.map_err(trap)?;
}
Ok(amt)
}
fn stage_am_media_type_dshow(
sb: &mut crate::Sandbox,
fourcc: [u8; 4],
width: i32,
height: i32,
) -> Result<u32> {
let to_oxide = |e: crate::Error| Error::other(format!("vfw discovery (DShow): stage AMT: {e}"));
let blob = sb
.host
.arena_alloc(72 + 88 + 16)
.map_err(|e| to_oxide(crate::Error::Win32(e)))?;
let amt = blob;
let fmt = blob + 72;
let mediatype_video =
crate::com::Guid::parse("{73646976-0000-0010-8000-00AA00389B71}").unwrap();
let format_videoinfo =
crate::com::Guid::parse("{05589F80-C356-11CE-BF01-00AA0055595A}").unwrap();
let d1 = u32::from_le_bytes(fourcc);
let subtype = crate::com::Guid::new(
d1,
0x0000,
0x0010,
[0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71],
);
let trap = |e: crate::emulator::Trap| to_oxide(crate::Error::Trap(e));
mediatype_video.stage(&mut sb.mmu, amt).map_err(trap)?;
subtype.stage(&mut sb.mmu, amt + 16).map_err(trap)?;
sb.mmu
.write_initializer(amt + 32, &1u32.to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(amt + 36, &1u32.to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(amt + 40, &0u32.to_le_bytes())
.map_err(trap)?;
format_videoinfo
.stage(&mut sb.mmu, amt + 44)
.map_err(trap)?;
sb.mmu
.write_initializer(amt + 60, &0u32.to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(amt + 64, &88u32.to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(amt + 68, &fmt.to_le_bytes())
.map_err(trap)?;
for i in 0..48u32 {
sb.mmu.store8(fmt + i, 0).map_err(trap)?;
}
let bih = fmt + 48;
sb.mmu
.write_initializer(bih, &40u32.to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(bih + 4, &(width as u32).to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(bih + 8, &(height as u32).to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(bih + 12, &1u16.to_le_bytes())
.map_err(trap)?;
sb.mmu
.write_initializer(bih + 14, &24u16.to_le_bytes())
.map_err(trap)?;
sb.mmu.write_initializer(bih + 16, &fourcc).map_err(trap)?;
let size_image = (width.unsigned_abs() * height.unsigned_abs() * 3) / 2;
sb.mmu
.write_initializer(bih + 20, &size_image.to_le_bytes())
.map_err(trap)?;
for off in [24u32, 28, 32, 36] {
sb.mmu
.write_initializer(bih + off, &0u32.to_le_bytes())
.map_err(trap)?;
}
Ok(amt)
}
fn first_input_pin(sb: &mut crate::Sandbox, filter: u32) -> Option<u32> {
pin_with_direction(sb, filter, crate::com::PIN_DIRECTION_INPUT, None)
}
fn pin_with_direction(
sb: &mut crate::Sandbox,
filter: u32,
direction: u32,
skip: Option<u32>,
) -> Option<u32> {
use crate::com::call::call_method;
let _ = call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
filter,
crate::com::SLOT_BASEFILTER_STOP,
&[],
);
let scratch = sb.host.arena_alloc(4).ok()?;
sb.mmu.write_initializer(scratch, &[0u8; 4]).ok()?;
let r = call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
filter,
crate::com::SLOT_BASEFILTER_ENUM_PINS,
&[scratch],
);
let pp = sb.mmu.load32(scratch).unwrap_or(0);
if !matches!(r, Ok(0)) || pp == 0 {
return None;
}
sb.host.com.intern(pp, None);
let mut pins: Vec<u32> = Vec::new();
for _ in 0..16 {
let pin_slot = sb.host.arena_alloc(8).ok()?;
sb.mmu.write_initializer(pin_slot, &[0u8; 8]).ok()?;
let r = call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
pp,
crate::com::SLOT_ENUMPINS_NEXT,
&[1, pin_slot, pin_slot + 4],
);
let pin = sb.mmu.load32(pin_slot).unwrap_or(0);
let fetched = sb.mmu.load32(pin_slot + 4).unwrap_or(0);
match r {
Ok(0) if pin != 0 && fetched == 1 => {
sb.host.com.intern(pin, None);
pins.push(pin);
}
Ok(1) => {
if pin != 0 && fetched == 1 {
sb.host.com.intern(pin, None);
pins.push(pin);
}
break;
}
_ => break,
}
}
let _ = sb.com_release(pp);
let mut chosen: Option<u32> = None;
for &pin in &pins {
if let Some(s) = skip {
if pin == s {
continue;
}
}
let dir_slot = sb.host.arena_alloc(4).ok()?;
let _ = sb.mmu.write_initializer(dir_slot, &0xFFu32.to_le_bytes());
let r = call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
pin,
crate::com::SLOT_PIN_QUERY_DIRECTION,
&[dir_slot],
);
if !matches!(r, Ok(0)) {
continue;
}
let dir = sb.mmu.load32(dir_slot).unwrap_or(u32::MAX);
if dir == direction {
chosen = Some(pin);
break;
}
}
for pin in pins {
if Some(pin) == chosen {
continue;
}
let _ = sb.com_release(pin);
}
chosen
}
impl Decoder for SandboxedDshowDecoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn send_packet(&mut self, packet: &Packet) -> Result<()> {
if self.pending.is_some() {
return Err(Error::other(
"vfw discovery (DShow): receive_frame must be called before sending another packet",
));
}
self.ensure_open()?;
self.pending = Some(packet.clone());
Ok(())
}
fn receive_frame(&mut self) -> Result<Frame> {
let packet = match self.pending.take() {
Some(p) => p,
None => {
return if self.eof {
Err(Error::Eof)
} else {
Err(Error::NeedMore)
};
}
};
let mip = self.mem_input_pin;
if mip == 0 {
return Err(Error::other(
"vfw discovery (DShow): IMemInputPin not bound; ensure_open did not complete",
));
}
let allocator = if self.using_codec_allocator {
self.codec_allocator
} else {
self.host_allocator
};
let from_codec = self.using_codec_allocator;
let sb = self
.sandbox
.as_mut()
.ok_or_else(|| Error::other("vfw discovery (DShow): no sandbox"))?;
let pp = sb
.host
.arena_alloc(4)
.map_err(|e| Error::other(format!("vfw discovery (DShow): arena: {e}")))?;
sb.mmu
.write_initializer(pp, &0u32.to_le_bytes())
.map_err(|e| Error::other(format!("vfw discovery (DShow): mmu init: {e}")))?;
let r_gb = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
allocator,
crate::com::SLOT_MEMALLOCATOR_GET_BUFFER,
&[pp, 0, 0, 0],
)
.map_err(|e| Error::other(format!("vfw discovery (DShow): GetBuffer trapped: {e}")))?;
if r_gb != 0 {
return Err(Error::other(format!(
"vfw discovery (DShow): {} allocator GetBuffer returned \
{r_gb:#010x} (pool exhausted?)",
if from_codec { "codec" } else { "host" }
)));
}
let sample = sb
.mmu
.load32(pp)
.map_err(|e| Error::other(format!("vfw discovery (DShow): load sample slot: {e}")))?;
if sample == 0 {
return Err(Error::other(format!(
"vfw discovery (DShow): {} GetBuffer succeeded but sample = 0",
if from_codec { "codec" } else { "host" }
)));
}
if from_codec {
let pp_buf = sb.host.arena_alloc(4).map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): codec sample GetPointer arena: {e}"
))
})?;
sb.mmu
.write_initializer(pp_buf, &0u32.to_le_bytes())
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): codec sample GetPointer init: {e}"
))
})?;
let r_gp = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
sample,
crate::com::SLOT_MEDIASAMPLE_GET_POINTER,
&[pp_buf],
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): codec sample GetPointer trapped: {e}"
))
})?;
if r_gp != crate::com::S_OK {
return Err(Error::other(format!(
"vfw discovery (DShow): codec sample GetPointer returned \
{r_gp:#010x}"
)));
}
let buf = sb.mmu.load32(pp_buf).unwrap_or(0);
if buf == 0 {
return Err(Error::other(
"vfw discovery (DShow): codec sample GetPointer wrote NULL buffer",
));
}
for (i, &b) in packet.data.iter().enumerate() {
sb.mmu.store8(buf + i as u32, b).map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): codec sample payload write: {e}"
))
})?;
}
let _ = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
sample,
crate::com::SLOT_MEDIASAMPLE_SET_ACTUAL_DATA_LENGTH,
&[packet.data.len() as u32],
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): codec sample SetActualDataLength: {e}"
))
})?;
let _ = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
sample,
crate::com::SLOT_MEDIASAMPLE_SET_SYNC_POINT,
&[u32::from(packet.flags.keyframe)],
)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): codec sample SetSyncPoint: {e}"
))
})?;
} else {
sb.media_sample_set_payload(sample, &packet.data, packet.flags.keyframe)
.map_err(|e| {
Error::other(format!(
"vfw discovery (DShow): media_sample_set_payload: {e}"
))
})?;
}
sb.cpu.enable_trace_ring(4096);
let r40_module_base = sb.host.primary_module_base;
for off in [
0x002626u32,
0x002634,
0x00263b,
0x0025a2,
0x0025a4,
0x0025a8,
0x0025ab,
0x0025ae,
0x002620,
0x002621,
0x006479,
0x0064a3,
0x0064f3,
0x006545,
0x00655e,
0x0065c0,
0x0065c4,
] {
sb.cpu
.add_register_watchpoint(r40_module_base.wrapping_add(off));
}
sb.cpu.register_snapshots_cap = 64;
let mut mip_state: Vec<String> = Vec::new();
for off in (0..=0xa0u32).step_by(4) {
if let Ok(v) = sb.mmu.load32(mip + off) {
mip_state.push(format!("[+{off:#04x}]={v:#010x}"));
}
}
log::debug!(
"vfw discovery (DShow): pre-Receive mip={mip:#010x} state: {:?}",
mip_state
);
let r38_mip = mip;
let mut sample_state: Vec<String> = Vec::new();
for off in (0..=0xa0u32).step_by(4) {
if let Ok(v) = sb.mmu.load32(sample + off) {
sample_state.push(format!("[+{off:#04x}]={v:#010x}"));
}
}
let sample_vtbl = sb.mmu.load32(sample).unwrap_or(0);
let sample_slot13 = if sample_vtbl != 0 {
sb.mmu.load32(sample_vtbl + 0x34).unwrap_or(0)
} else {
0
};
let filter_base = self.filter.wrapping_sub(0xc);
let filter_primary_vtbl = sb.mmu.load32(filter_base).unwrap_or(0);
let filter_pin_in = sb.mmu.load32(filter_base.wrapping_add(0x8c)).unwrap_or(0);
let filter_pin_out = sb.mmu.load32(filter_base.wrapping_add(0x90)).unwrap_or(0);
log::debug!(
"vfw discovery (DShow): r38 pre-Receive sanity: \
sample={sample:#010x} sample[+0]={sample_vtbl:#010x} \
sample_vtbl[+0x34]={sample_slot13:#010x} \
self.filter={:#010x} filter_base={filter_base:#010x} \
[filter_base+0]={filter_primary_vtbl:#010x} \
[filter_base+0x8c]={filter_pin_in:#010x} \
[filter_base+0x90]={filter_pin_out:#010x}",
self.filter,
);
let r38_force_target = filter_base; if filter_pin_in == 0 {
log::debug!(
"vfw discovery (DShow): r38 [filter_base+0x8c]=NULL — \
calling filter_base->primary_slot7 to force input-pin \
allocation"
);
let _ = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
r38_force_target,
7,
&[0],
);
let now = sb.mmu.load32(filter_base.wrapping_add(0x8c)).unwrap_or(0);
log::debug!("vfw discovery (DShow): r38 post-force [filter_base+0x8c]={now:#010x}");
}
let output_alloc = if filter_pin_out != 0 {
sb.mmu.load32(filter_pin_out + 0x98).unwrap_or(0)
} else {
0
};
let output_alloc_vtbl0 = if output_alloc != 0 {
sb.mmu.load32(output_alloc).unwrap_or(0)
} else {
0
};
let output_alloc_qi_thunk = if output_alloc_vtbl0 != 0 {
sb.mmu.load32(output_alloc_vtbl0).unwrap_or(0)
} else {
0
};
let mut r43_oalloc_state: Vec<String> = Vec::new();
if output_alloc != 0 {
for off in (0..=0x3cu32).step_by(4) {
if let Ok(v) = sb.mmu.load32(output_alloc + off) {
r43_oalloc_state.push(format!("[+{off:#04x}]={v:#010x}"));
}
}
}
let mut r43_oalloc_pool: Vec<String> = Vec::new();
if output_alloc != 0 {
let mut cur = sb.mmu.load32(output_alloc + 8).unwrap_or(0);
for i in 0..6u32 {
if cur == 0 {
r43_oalloc_pool.push(format!("[{i}]=NULL"));
break;
}
let in_use = sb.mmu.load32(cur + 36).unwrap_or(0xDEAD_BEEF);
let next = sb.mmu.load32(cur + 32).unwrap_or(0xDEAD_BEEF);
r43_oalloc_pool.push(format!(
"[{i}]={cur:#010x} in_use={in_use:#x} next={next:#010x}"
));
cur = next;
}
}
let r38_pre_receive = format!(
"sample={sample:#010x} sample_vtbl={sample_vtbl:#010x} \
sample_vtbl[+0x34]={sample_slot13:#010x} \
mip={r38_mip:#010x} self.filter={:#010x} \
filter_base={filter_base:#010x} \
[filter_base+0]={filter_primary_vtbl:#010x} \
[filter_base+0x8c]={filter_pin_in:#010x} \
[filter_base+0x90]={filter_pin_out:#010x} \
output_alloc={output_alloc:#010x} \
output_alloc_vtbl0={output_alloc_vtbl0:#010x} \
output_alloc_qi_thunk={output_alloc_qi_thunk:#010x} \
r43_oalloc_state={r43_oalloc_state:?} \
r43_oalloc_pool={r43_oalloc_pool:?} \
sample_state={sample_state:?}",
self.filter,
);
let r_recv = match crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
mip,
crate::com::SLOT_MEMINPUTPIN_RECEIVE,
&[sample],
) {
Ok(v) => v,
Err(e) => {
use crate::emulator::regs::Reg32;
let ring = sb.cpu.trace_ring.clone();
let trap_eip = ring.last().copied().unwrap_or(sb.cpu.regs.eip);
let module_base = sb.host.primary_module_base;
let rva = trap_eip.wrapping_sub(module_base);
let trap_eax = sb.cpu.regs.get32(Reg32::Eax);
let trap_ecx = sb.cpu.regs.get32(Reg32::Ecx);
let trap_edx = sb.cpu.regs.get32(Reg32::Edx);
let trap_ebx = sb.cpu.regs.get32(Reg32::Ebx);
let trap_ebp = sb.cpu.regs.get32(Reg32::Ebp);
let trap_esi = sb.cpu.regs.get32(Reg32::Esi);
let trap_edi = sb.cpu.regs.get32(Reg32::Edi);
let ring_tail: Vec<String> = ring
.iter()
.rev()
.take(8)
.map(|e| format!("{e:#010x}"))
.collect();
let mut call_chain: Vec<u32> = Vec::new();
let mut prev: u32 = 0;
for &eip in ring.iter() {
let rva_e = eip.wrapping_sub(module_base);
if prev == 0 || (eip >= prev + 0x40) || (eip + 0x100 < prev) {
call_chain.push(rva_e);
}
prev = eip;
}
let chain_tail: Vec<String> = call_chain
.iter()
.rev()
.take(24)
.rev()
.map(|e| format!("{e:#010x}"))
.collect();
let raw_tail: Vec<String> = ring
.iter()
.rev()
.take(32)
.rev()
.map(|e| format!("{:#010x}", e.wrapping_sub(module_base)))
.collect();
let recheck_sample_vtbl = sb.mmu.load32(sample).unwrap_or(0);
let recheck_sample_slot13 = if recheck_sample_vtbl != 0 {
sb.mmu
.load32(recheck_sample_vtbl.wrapping_add(0x34))
.unwrap_or(0)
} else {
0
};
let r40_mem_raw = sb.cpu.take_memory_snapshots();
let r40_snaps_raw = sb.cpu.clear_register_watchpoints();
let r40_snaps: Vec<String> = r40_snaps_raw
.iter()
.map(|(eip, regs)| {
let r_rva = eip.wrapping_sub(module_base);
format!(
"rva={r_rva:#06x} eax={:#010x} ecx={:#010x} \
edx={:#010x} ebx={:#010x} esp={:#010x} \
ebp={:#010x} esi={:#010x} edi={:#010x}",
regs[0], regs[1], regs[2], regs[3], regs[4], regs[5], regs[6], regs[7]
)
})
.collect();
let mut r40_arg1: Vec<String> = Vec::new();
for (eip, mem) in &r40_mem_raw {
let r_rva = eip.wrapping_sub(module_base);
let p_esp = mem[0].1;
let p_esp4 = mem[1].1;
let p_ebp_p8 = mem[2].1;
let p_ebp_m50 = mem[3].1;
let a_ebp_p8 = mem[2].0;
let a_ebp_m50 = mem[3].0;
r40_arg1.push(format!(
"rva={r_rva:#06x} [ebp+8]@{a_ebp_p8:#010x}={p_ebp_p8:#010x} \
[esp]={p_esp:#010x} [esp+4]={p_esp4:#010x} \
[ebp-0x50]@{a_ebp_m50:#010x}={p_ebp_m50:#010x}"
));
}
let r40_expected_pin_sample = sample;
let r40_expected_filter_base = self.filter.wrapping_sub(0xc);
let esp = sb.cpu.regs.esp();
let mut stack_frame: Vec<String> = Vec::new();
for i in 0..32u32 {
if let Ok(v) = sb.mmu.load32(esp + i * 4) {
let v_rva = v.wrapping_sub(module_base);
stack_frame.push(format!(
"[esp+{:02x}]={:#010x} (rva={:#010x})",
i * 4,
v,
v_rva
));
}
}
return Err(Error::other(format!(
"vfw discovery (DShow): Receive trapped: {e} \
[trap_eip={trap_eip:#010x} rva={rva:#010x} \
eax={trap_eax:#010x} ecx={trap_ecx:#010x} \
edx={trap_edx:#010x} ebx={trap_ebx:#010x} \
esp={esp:#010x} ebp={trap_ebp:#010x} \
esi={trap_esi:#010x} edi={trap_edi:#010x} \
recheck_sample_vtbl={recheck_sample_vtbl:#010x} \
recheck_sample_slot13={recheck_sample_slot13:#010x} \
r40_expected_pin_sample={r40_expected_pin_sample:#010x} \
r40_expected_filter_base={r40_expected_filter_base:#010x} \
r40_snaps={r40_snaps:?} r40_arg1={r40_arg1:?} \
trace_tail={ring_tail:?} call_chain={chain_tail:?} \
raw_tail={raw_tail:?} stack={stack_frame:?} \
mip_state={mip_state:?} r38_pre={r38_pre_receive}]"
)));
}
};
let _ = sb.cpu.take_memory_snapshots();
let _ = sb.cpu.clear_register_watchpoints();
let _ = crate::com::call::call_method(
&mut sb.cpu,
&mut sb.mmu,
&sb.registry,
&mut sb.host,
allocator,
crate::com::SLOT_MEMALLOCATOR_RELEASE_BUFFER,
&[sample],
);
if let Some(rs) = sb.pop_received_sample() {
return surface_received_dshow_frame(rs, packet.pts, self.width, self.height);
}
let ring = sb.cpu.trace_ring.clone();
let ring_summary = if ring.is_empty() {
String::from("empty")
} else {
let head: Vec<String> = ring.iter().take(4).map(|e| format!("{e:#010x}")).collect();
let tail: Vec<String> = ring
.iter()
.rev()
.take(4)
.map(|e| format!("{e:#010x}"))
.collect();
format!("len={} head={:?} tail={:?}", ring.len(), head, tail)
};
if r_recv == 0 {
log::debug!(
"vfw discovery (DShow): Receive ok but no output sample queued \
(trace_ring {ring_summary})"
);
Err(Error::Eof)
} else {
Err(Error::unsupported(format!(
"vfw discovery (DShow): IMemInputPin::Receive → {r_recv:#010x}; \
no decoded sample emitted (trace_ring {ring_summary})"
)))
}
}
fn flush(&mut self) -> Result<()> {
self.eof = true;
Ok(())
}
}
impl Drop for SandboxedDshowDecoder {
fn drop(&mut self) {
if let Some(sb) = self.sandbox.as_mut() {
if self.mem_input_pin != 0 {
let _ = sb.com_release(self.mem_input_pin);
}
if self.input_pin != 0 {
let _ = sb.com_release(self.input_pin);
}
if self.filter != 0 {
let _ = sb.com_release(self.filter);
}
crate::com::host_iface_r31::clear_queue(&sb.host);
}
}
}
fn surface_received_dshow_frame(
rs: crate::com::host_iface_r31::ReceivedSample,
pts: Option<i64>,
width: u32,
height: u32,
) -> Result<Frame> {
let w = width as usize;
let h = height as usize;
let stride = w * 3;
let expected = stride * h;
let raw = if rs.data.len() >= expected {
rs.data[..expected].to_vec()
} else {
let mut padded = vec![0u8; expected];
padded[..rs.data.len()].copy_from_slice(&rs.data);
padded
};
let mut data = vec![0u8; expected];
if h > 0 && stride > 0 {
for row in 0..h {
let src_off = (h - 1 - row) * stride;
let dst_off = row * stride;
data[dst_off..dst_off + stride].copy_from_slice(&raw[src_off..src_off + stride]);
}
}
Ok(Frame::Video(VideoFrame {
pts,
planes: vec![VideoPlane { stride, data }],
}))
}
pub fn output_pixel_format() -> PixelFormat {
PixelFormat::Bgr24
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn codec_id_format_matches_spec() {
assert_eq!(
codec_id_for(&PathBuf::from("/tmp/MPG4DS32.AX"), "MP43"),
"vfw_mp43_mpg4ds32"
);
assert_eq!(
codec_id_for(&PathBuf::from("foo/IR32_32.DLL"), "IV31"),
"vfw_iv31_ir32_32"
);
}
#[test]
fn codec_id_sanitises_dashes_and_dots() {
assert_eq!(
codec_id_for(&PathBuf::from("/p/some-name.with.dot.dll"), "ABCD"),
"vfw_abcd_some_name_with_dot"
);
}
#[test]
fn record_table_round_trip() {
let id = "vfw_test_record_round_trip_unique";
register_factory_for_id(
id,
DiscoveryRecord {
dll_path: PathBuf::from("/x/y.dll"),
fourcc: "MP43".to_string(),
kind: Kind::Vfw,
clsid: None,
},
);
let r = lookup_record(id).expect("present");
assert_eq!(r.fourcc, "MP43");
assert_eq!(r.kind, Kind::Vfw);
}
#[test]
fn make_decoder_unknown_id_errors_cleanly() {
let params = CodecParameters::video(CodecId::new(
"vfw_unknown_codec_id_should_not_match_anything",
));
let r = make_decoder(¶ms);
assert!(r.is_err());
}
#[test]
fn make_decoder_dshow_constructs_decoder_without_dll() {
let id = "vfw_dshow_make_decoder_test_round30";
register_factory_for_id(
id,
DiscoveryRecord {
dll_path: PathBuf::from("/dev/null"),
fourcc: "WMV3".to_string(),
kind: Kind::DirectShow,
clsid: Some("{82CCD3E0-F71A-11D0-9FE5-00609778EA66}".into()),
},
);
let params = CodecParameters::video(CodecId::new(id));
let decoder = make_decoder(¶ms).expect("DShow make_decoder constructs lazily");
assert_eq!(decoder.codec_id().as_str(), id);
}
}