feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple trim bugs the stub was hiding); same-track via the frozen C ABI, cross-track supported by the module command - oaknode clip blocks now declare a tex_in texture input and set effect_input to it, so timeline clips can host effect chains; facade test covers effect insert/remove on a real clip - oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a Homebrew upgrade left the system ffmpeg .pc pointing at a deleted dav1d Cellar path, breaking links); reads a git-ignored workspace .env for IDEs that cannot inject env vars (RustRover); links the C++ stdlib for C++ codec libs (svt-av1) - oakengine re-exports oaknode so tests share one crate instance; it_node uses the direct instance's value type where it calls the module FFI (the --workspace dev-dependency feature split builds oaknode twice)
This commit is contained in:
@@ -320,7 +320,10 @@ mod tests {
|
||||
assert!(c.attach(0).is_err(), "zero identity rejected");
|
||||
c.attach(42).unwrap();
|
||||
assert_eq!(c.copied_project, 42);
|
||||
c.on_cache_request(42, TimeRange::new(Rational::new(0, 1), Rational::new(10, 1)));
|
||||
c.on_cache_request(
|
||||
42,
|
||||
TimeRange::new(Rational::new(0, 1), Rational::new(10, 1)),
|
||||
);
|
||||
assert_eq!(c.live_jobs().len(), 1);
|
||||
c.detach();
|
||||
assert_eq!(c.copied_project, 0);
|
||||
|
||||
+117
-69
@@ -98,8 +98,8 @@ impl BackendKind {
|
||||
return BackendKind::from_config_string(&v);
|
||||
}
|
||||
}
|
||||
let configured = crate::bridge::common::config_get_string(None, "GraphicsBackend")
|
||||
.unwrap_or_default();
|
||||
let configured =
|
||||
crate::bridge::common::config_get_string(None, "GraphicsBackend").unwrap_or_default();
|
||||
BackendKind::from_config_string(&configured)
|
||||
}
|
||||
|
||||
@@ -107,13 +107,25 @@ impl BackendKind {
|
||||
fn wgpu_fallbacks(self) -> Vec<wgpu::Backends> {
|
||||
match self {
|
||||
BackendKind::Auto | BackendKind::Metal => {
|
||||
vec![wgpu::Backends::METAL, wgpu::Backends::VULKAN, wgpu::Backends::GL]
|
||||
vec![
|
||||
wgpu::Backends::METAL,
|
||||
wgpu::Backends::VULKAN,
|
||||
wgpu::Backends::GL,
|
||||
]
|
||||
}
|
||||
BackendKind::Vulkan => {
|
||||
vec![wgpu::Backends::VULKAN, wgpu::Backends::METAL, wgpu::Backends::GL]
|
||||
vec![
|
||||
wgpu::Backends::VULKAN,
|
||||
wgpu::Backends::METAL,
|
||||
wgpu::Backends::GL,
|
||||
]
|
||||
}
|
||||
BackendKind::Gl => {
|
||||
vec![wgpu::Backends::GL, wgpu::Backends::METAL, wgpu::Backends::VULKAN]
|
||||
vec![
|
||||
wgpu::Backends::GL,
|
||||
wgpu::Backends::METAL,
|
||||
wgpu::Backends::VULKAN,
|
||||
]
|
||||
}
|
||||
BackendKind::Cpu => Vec::new(),
|
||||
}
|
||||
@@ -193,29 +205,27 @@ impl GpuContext {
|
||||
backends,
|
||||
..Default::default()
|
||||
});
|
||||
let adapter = match pollster_block_on(instance.request_adapter(
|
||||
&wgpu::RequestAdapterOptions {
|
||||
let adapter =
|
||||
match pollster_block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||
compatible_surface: None,
|
||||
force_fallback_adapter: false,
|
||||
},
|
||||
)) {
|
||||
Ok(a) => a,
|
||||
Err(_) => continue, // try the next backend in the fallback order
|
||||
};
|
||||
})) {
|
||||
Ok(a) => a,
|
||||
Err(_) => continue, // try the next backend in the fallback order
|
||||
};
|
||||
let info = adapter.get_info();
|
||||
let (device, queue) = match pollster_block_on(adapter.request_device(
|
||||
&wgpu::DeviceDescriptor {
|
||||
let (device, queue) =
|
||||
match pollster_block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
label: Some("oakrender"),
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_limits: wgpu::Limits::default(),
|
||||
memory_hints: wgpu::MemoryHints::default(),
|
||||
trace: wgpu::Trace::Off,
|
||||
},
|
||||
)) {
|
||||
Ok(dq) => dq,
|
||||
Err(_) => continue,
|
||||
};
|
||||
})) {
|
||||
Ok(dq) => dq,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let kind = BackendKind::from_wgpu_backend(info.backend);
|
||||
if kind == BackendKind::Cpu {
|
||||
continue;
|
||||
@@ -346,7 +356,7 @@ impl GpuContext {
|
||||
let w = entry.width as usize;
|
||||
let h = entry.height as usize;
|
||||
let linesize = w * 4 * 4; // Rgba32Float
|
||||
// copy_texture_to_buffer requires a 256-byte-aligned row stride.
|
||||
// copy_texture_to_buffer requires a 256-byte-aligned row stride.
|
||||
let padded = (linesize + 255) & !255;
|
||||
|
||||
let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
@@ -387,9 +397,11 @@ impl GpuContext {
|
||||
// Map + block until the callback fires (headless-safe: no surface,
|
||||
// no event loop; PollType::Wait drives the callbacks).
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
buffer.slice(..).map_async(wgpu::MapMode::Read, move |result| {
|
||||
let _ = tx.send(result.is_ok());
|
||||
});
|
||||
buffer
|
||||
.slice(..)
|
||||
.map_async(wgpu::MapMode::Read, move |result| {
|
||||
let _ = tx.send(result.is_ok());
|
||||
});
|
||||
let _ = self.device.poll(wgpu::PollType::wait());
|
||||
if !rx.recv().unwrap_or(false) {
|
||||
return Err(Error::Failed("texture download map failed".into()));
|
||||
@@ -446,16 +458,14 @@ impl GpuContext {
|
||||
|
||||
let src_view = src_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let dst_view = dst_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let bind_group = self
|
||||
.device
|
||||
.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("oakrender-blit-bg"),
|
||||
layout: &self.blit_bind_group_layout(),
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&src_view),
|
||||
}],
|
||||
});
|
||||
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("oakrender-blit-bg"),
|
||||
layout: &self.blit_bind_group_layout(),
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&src_view),
|
||||
}],
|
||||
});
|
||||
|
||||
let mut encoder = self
|
||||
.device
|
||||
@@ -486,26 +496,29 @@ impl GpuContext {
|
||||
}
|
||||
|
||||
fn blit_bind_group_layout(&self) -> wgpu::BindGroupLayout {
|
||||
self.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("oakrender-blit-layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
})
|
||||
self.device
|
||||
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("oakrender-blit-layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
fn create_blit_pipeline(&self) -> Result<wgpu::RenderPipeline> {
|
||||
let shader = self.device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("oakrender-blit-shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(BLIT_WGSL)),
|
||||
});
|
||||
let shader = self
|
||||
.device
|
||||
.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("oakrender-blit-shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(BLIT_WGSL)),
|
||||
});
|
||||
let layout = self.blit_bind_group_layout();
|
||||
let pipeline_layout = self
|
||||
.device
|
||||
@@ -759,7 +772,12 @@ impl DisplayRenderer {
|
||||
}
|
||||
|
||||
/// Upload pixels into a texture (GPU: backend upload; CPU: buffer copy).
|
||||
pub fn upload_texture(&self, texture: &mut Texture, pixels: *const u8, linesize: usize) -> Result<()> {
|
||||
pub fn upload_texture(
|
||||
&self,
|
||||
texture: &mut Texture,
|
||||
pixels: *const u8,
|
||||
linesize: usize,
|
||||
) -> Result<()> {
|
||||
let size = texture.size();
|
||||
match texture {
|
||||
Texture::Gpu { token, ctx, .. } => {
|
||||
@@ -772,9 +790,9 @@ impl DisplayRenderer {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let h = frame.height as usize;
|
||||
frame.data.copy_from_slice(unsafe {
|
||||
std::slice::from_raw_parts(pixels, stride * h)
|
||||
});
|
||||
frame
|
||||
.data
|
||||
.copy_from_slice(unsafe { std::slice::from_raw_parts(pixels, stride * h) });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -804,11 +822,14 @@ impl DisplayRenderer {
|
||||
processor: Option<&crate::color::ColorProcessor>,
|
||||
) -> Result<()> {
|
||||
match (src, dst) {
|
||||
(Some(Texture::Gpu { token: s, ctx, .. }), Texture::Gpu { token: d, ctx: dctx, .. })
|
||||
if Arc::ptr_eq(ctx, dctx) =>
|
||||
{
|
||||
ctx.blit(*s, *d, processor)
|
||||
}
|
||||
(
|
||||
Some(Texture::Gpu { token: s, ctx, .. }),
|
||||
Texture::Gpu {
|
||||
token: d,
|
||||
ctx: dctx,
|
||||
..
|
||||
},
|
||||
) if Arc::ptr_eq(ctx, dctx) => ctx.blit(*s, *d, processor),
|
||||
(Some(Texture::Cpu(sf)), Texture::Cpu(df)) => {
|
||||
if sf.width != df.width || sf.height != df.height {
|
||||
return Err(Error::Invalid);
|
||||
@@ -835,7 +856,10 @@ impl DisplayRenderer {
|
||||
dst: *mut u8,
|
||||
linesize: usize,
|
||||
) -> Result<()> {
|
||||
let ctx = self.ctx.as_ref().ok_or(Error::Failed("no GPU context".into()))?;
|
||||
let ctx = self
|
||||
.ctx
|
||||
.as_ref()
|
||||
.ok_or(Error::Failed("no GPU context".into()))?;
|
||||
let frame = ctx.download(texture_id as u64)?;
|
||||
let want_linesize = params.effective_width() as usize * 4 * 4;
|
||||
if linesize != want_linesize {
|
||||
@@ -901,12 +925,18 @@ mod tests {
|
||||
}
|
||||
// C++ legacy values map onto the CPU fallback (documented).
|
||||
assert_eq!(BackendKind::from_config_string("dummy"), BackendKind::Cpu);
|
||||
assert_eq!(BackendKind::from_config_string("multiprocess"), BackendKind::Cpu);
|
||||
assert_eq!(
|
||||
BackendKind::from_config_string("multiprocess"),
|
||||
BackendKind::Cpu
|
||||
);
|
||||
// Unknown → Auto.
|
||||
assert_eq!(BackendKind::from_config_string("bogus"), BackendKind::Auto);
|
||||
assert_eq!(BackendKind::from_config_string(""), BackendKind::Auto);
|
||||
// to_config_string round-trips.
|
||||
assert_eq!(BackendKind::from_config_string(BackendKind::Metal.to_config_string()), BackendKind::Metal);
|
||||
assert_eq!(
|
||||
BackendKind::from_config_string(BackendKind::Metal.to_config_string()),
|
||||
BackendKind::Metal
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -984,7 +1014,13 @@ mod tests {
|
||||
let out = ctx.download(dst).unwrap();
|
||||
assert_eq!(out.data, frame.data, "plain-copy blit is pixel-exact");
|
||||
// Color-managed blit is documented-deferred.
|
||||
assert!(ctx.blit(src, dst, Some(&crate::color::ColorProcessor::pass_through())).is_err());
|
||||
assert!(ctx
|
||||
.blit(
|
||||
src,
|
||||
dst,
|
||||
Some(&crate::color::ColorProcessor::pass_through())
|
||||
)
|
||||
.is_err());
|
||||
ctx.destroy_texture(src);
|
||||
ctx.destroy_texture(dst);
|
||||
}
|
||||
@@ -1052,8 +1088,12 @@ mod tests {
|
||||
frame.allocate();
|
||||
frame.data[0] = 0x77;
|
||||
let mut upload_target = r3.create_texture(&pod, None).unwrap();
|
||||
r3.upload_texture(&mut upload_target, frame.data.as_ptr(), frame.linesize_bytes())
|
||||
.unwrap();
|
||||
r3.upload_texture(
|
||||
&mut upload_target,
|
||||
frame.data.as_ptr(),
|
||||
frame.linesize_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut buf = vec![0u8; frame.linesize_bytes() * 4];
|
||||
r3.download_texture(&upload_target, buf.as_mut_ptr(), frame.linesize_bytes())
|
||||
.unwrap();
|
||||
@@ -1072,17 +1112,25 @@ mod tests {
|
||||
pod.height = 2;
|
||||
let mut src = r.create_texture(&pod, None).unwrap();
|
||||
let mut dst = r.create_texture(&pod, None).unwrap();
|
||||
let Texture::Cpu(sf) = &mut src else { unreachable!() };
|
||||
let Texture::Cpu(sf) = &mut src else {
|
||||
unreachable!()
|
||||
};
|
||||
sf.data[0] = 0x11;
|
||||
sf.data[4] = 0x22;
|
||||
// Plain copy.
|
||||
r.blit_color_managed(Some(&src), &mut dst, None).unwrap();
|
||||
let Texture::Cpu(df) = &dst else { unreachable!() };
|
||||
let Texture::Cpu(df) = &dst else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(df.data[0], 0x11);
|
||||
assert_eq!(df.data[4], 0x22);
|
||||
// Pass-through processor is a no-op.
|
||||
r.blit_color_managed(Some(&src), &mut dst, Some(&crate::color::ColorProcessor::pass_through()))
|
||||
.unwrap();
|
||||
r.blit_color_managed(
|
||||
Some(&src),
|
||||
&mut dst,
|
||||
Some(&crate::color::ColorProcessor::pass_through()),
|
||||
)
|
||||
.unwrap();
|
||||
// Size mismatch rejected.
|
||||
let mut pod2 = VideoParamsPod::default();
|
||||
pod2.width = 3;
|
||||
|
||||
@@ -30,7 +30,12 @@ pub fn config_get_string(group: Option<&str>, key: &str) -> Option<String> {
|
||||
let key_c = std::ffi::CString::new(key).ok()?;
|
||||
let group_ptr = || group_c.as_ref().map_or(std::ptr::null(), |c| c.as_ptr());
|
||||
let size = unsafe {
|
||||
oakcommon::ffi::config::oakcommon_config_get(group_ptr(), key_c.as_ptr(), std::ptr::null_mut(), 0)
|
||||
oakcommon::ffi::config::oakcommon_config_get(
|
||||
group_ptr(),
|
||||
key_c.as_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if size <= 1 {
|
||||
return None; // missing or empty
|
||||
@@ -94,7 +99,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn configuration_location_uses_env_override() {
|
||||
let _guard = crate::bridge::common::ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::bridge::common::ENV_TEST_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let dir = std::env::temp_dir().join("oakrender-config-test");
|
||||
std::env::set_var("OAK_CONFIG_DIR", &dir);
|
||||
let loc = configuration_location();
|
||||
@@ -104,7 +111,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn default_disk_cache_path_is_under_config() {
|
||||
let _guard = crate::bridge::common::ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::bridge::common::ENV_TEST_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let dir = std::env::temp_dir().join("oakrender-cache-test");
|
||||
std::env::set_var("OAK_CONFIG_DIR", &dir);
|
||||
let p = default_disk_cache_path();
|
||||
|
||||
@@ -80,12 +80,8 @@ pub fn project_sync_copy(
|
||||
copy: ProjectHandle,
|
||||
changes: &[ChangeRecord],
|
||||
) -> Result<()> {
|
||||
type F = unsafe extern "C" fn(
|
||||
ProjectHandle,
|
||||
ProjectHandle,
|
||||
*const ChangeRecord,
|
||||
c_int,
|
||||
) -> c_int;
|
||||
type F =
|
||||
unsafe extern "C" fn(ProjectHandle, ProjectHandle, *const ChangeRecord, c_int) -> c_int;
|
||||
let rc = crate::bridge::dlsym::call::<F, c_int>("oaknode_project_sync_copy", |f| unsafe {
|
||||
f(source, copy, changes.as_ptr(), changes.len() as c_int)
|
||||
})
|
||||
@@ -105,14 +101,17 @@ pub fn project_sync_copy(
|
||||
/// `node` must be a valid handle; `out` a valid pointer.
|
||||
pub unsafe fn node_get_video_frame_cache(node: NodeHandle, out: *mut CHandle) -> Result<()> {
|
||||
type F = unsafe extern "C" fn(NodeHandle, *mut CHandle) -> c_int;
|
||||
let rc = crate::bridge::dlsym::call::<F, c_int>("oaknode_node_get_video_frame_cache", |f| unsafe {
|
||||
f(node, out)
|
||||
})
|
||||
.ok_or_else(|| Error::Failed("oaknode_node_get_video_frame_cache missing".into()))?;
|
||||
let rc =
|
||||
crate::bridge::dlsym::call::<F, c_int>("oaknode_node_get_video_frame_cache", |f| unsafe {
|
||||
f(node, out)
|
||||
})
|
||||
.ok_or_else(|| Error::Failed("oaknode_node_get_video_frame_cache missing".into()))?;
|
||||
if rc == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Failed(format!("oaknode_node_get_video_frame_cache rc={rc}")))
|
||||
Err(Error::Failed(format!(
|
||||
"oaknode_node_get_video_frame_cache rc={rc}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +157,10 @@ mod tests {
|
||||
};
|
||||
assert_eq!(std::mem::size_of::<ChangeRecord>(), 4 + 48);
|
||||
let bytes: [u8; 52] = unsafe { std::mem::transmute(c) };
|
||||
assert_eq!(u32::from_le_bytes(bytes[0..4].try_into().unwrap()), change_kind::VALUE_CHANGE);
|
||||
assert_eq!(
|
||||
u32::from_le_bytes(bytes[0..4].try_into().unwrap()),
|
||||
change_kind::VALUE_CHANGE
|
||||
);
|
||||
assert_eq!(bytes[4], 7);
|
||||
}
|
||||
|
||||
|
||||
@@ -701,17 +701,17 @@ mod tests {
|
||||
let r = TimeRange::new(Rational::new(0, 1), Rational::new(5, 1));
|
||||
c.validate(r);
|
||||
c.invalidate(TimeRange::new(Rational::new(2, 1), Rational::new(2, 1)));
|
||||
assert!(c.has_validated_ranges(), "zero-length invalidate is a no-op");
|
||||
assert!(
|
||||
c.has_validated_ranges(),
|
||||
"zero-length invalidate is a no-op"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalidated_ranges_clamps_below_zero() {
|
||||
let mut c = tb_cache();
|
||||
c.validate(TimeRange::new(Rational::new(0, 1), Rational::new(5, 1)));
|
||||
let inv = c.invalidated_ranges(TimeRange::new(
|
||||
Rational::new(-5, 1),
|
||||
Rational::new(10, 1),
|
||||
));
|
||||
let inv = c.invalidated_ranges(TimeRange::new(Rational::new(-5, 1), Rational::new(10, 1)));
|
||||
// Only [5,10) remains after the clamp + validation removal.
|
||||
assert_eq!(inv.ranges().len(), 1);
|
||||
assert_eq!(inv.ranges()[0].in_(), Rational::new(5, 1));
|
||||
@@ -730,10 +730,7 @@ mod tests {
|
||||
assert_eq!(a.passthroughs().len(), 1);
|
||||
assert_eq!(a.passthroughs()[0].1, b.uuid);
|
||||
|
||||
let inv = a.invalidated_ranges(TimeRange::new(
|
||||
Rational::new(0, 1),
|
||||
Rational::new(10, 1),
|
||||
));
|
||||
let inv = a.invalidated_ranges(TimeRange::new(Rational::new(0, 1), Rational::new(10, 1)));
|
||||
assert_eq!(inv.ranges().len(), 1);
|
||||
assert_eq!(inv.ranges()[0].in_(), Rational::new(5, 1));
|
||||
|
||||
@@ -779,17 +776,15 @@ mod tests {
|
||||
assert_eq!(c.requested_ranges().ranges().len(), 1);
|
||||
c.request(TimeRange::new(Rational::new(3, 1), Rational::new(8, 1)));
|
||||
assert_eq!(c.requested_ranges().ranges().len(), 1, "merges on insert");
|
||||
assert_eq!(
|
||||
c.requested_ranges().ranges()[0].in_(),
|
||||
Rational::new(0, 1)
|
||||
);
|
||||
assert_eq!(c.requested_ranges().ranges()[0].in_(), Rational::new(0, 1));
|
||||
c.clear_request_range(TimeRange::new(Rational::new(2, 1), Rational::new(3, 1)));
|
||||
assert_eq!(c.requested_ranges().ranges().len(), 2, "splits on clear");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_cache_disk_state_uses_base_format() {
|
||||
let dir = std::env::temp_dir().join(format!("oakrender-audio-test-{}", next_owner_identity()));
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("oakrender-audio-test-{}", next_owner_identity()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut c = PlaybackCache::new(CacheKind::AudioPlayback, 1);
|
||||
c.set_saving_enabled(false);
|
||||
|
||||
@@ -75,7 +75,9 @@ impl ColorProcessor {
|
||||
pub fn create(src_space: &str, dst_transform: &str, dir: Direction) -> Option<Self> {
|
||||
let config = default_config()?;
|
||||
let src = if config.has_role(src_space) {
|
||||
config.canonical_name(src_space).unwrap_or_else(|| src_space.to_string())
|
||||
config
|
||||
.canonical_name(src_space)
|
||||
.unwrap_or_else(|| src_space.to_string())
|
||||
} else {
|
||||
src_space.to_string()
|
||||
};
|
||||
@@ -85,9 +87,14 @@ impl ColorProcessor {
|
||||
Direction::Inverse => config.processor(dst_transform, &src),
|
||||
}
|
||||
.ok();
|
||||
let cpu = processor.as_ref().and_then(|p| p.default_cpu_processor().ok());
|
||||
let cpu = processor
|
||||
.as_ref()
|
||||
.and_then(|p| p.default_cpu_processor().ok());
|
||||
|
||||
Some(Self { inner: processor, cpu })
|
||||
Some(Self {
|
||||
inner: processor,
|
||||
cpu,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create from a LUT file (C++ `create_lut` semantics): an OCIO
|
||||
@@ -101,8 +108,13 @@ impl ColorProcessor {
|
||||
let processor = config
|
||||
.processor_from_transform(&transform, dir.to_ocio())
|
||||
.ok();
|
||||
let cpu = processor.as_ref().and_then(|p| p.default_cpu_processor().ok());
|
||||
Some(Self { inner: processor, cpu })
|
||||
let cpu = processor
|
||||
.as_ref()
|
||||
.and_then(|p| p.default_cpu_processor().ok());
|
||||
Some(Self {
|
||||
inner: processor,
|
||||
cpu,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create from a LUT file on a specific config (C++
|
||||
@@ -115,22 +127,33 @@ impl ColorProcessor {
|
||||
let processor = config
|
||||
.processor_from_transform(&transform, dir.to_ocio())
|
||||
.ok();
|
||||
let cpu = processor.as_ref().and_then(|p| p.default_cpu_processor().ok());
|
||||
Some(Self { inner: processor, cpu })
|
||||
let cpu = processor
|
||||
.as_ref()
|
||||
.and_then(|p| p.default_cpu_processor().ok());
|
||||
Some(Self {
|
||||
inner: processor,
|
||||
cpu,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a dynamic grading-primary processor on the default config
|
||||
/// (C++ `ColorProcessor` grading-primary family).
|
||||
pub fn create_grading_primary(style: GradingStyle) -> Option<Self> {
|
||||
let config = default_config()?;
|
||||
let transform = ocio_rs::transform::GradingPrimaryTransform::create(style.to_ocio()).ok()?;
|
||||
let transform =
|
||||
ocio_rs::transform::GradingPrimaryTransform::create(style.to_ocio()).ok()?;
|
||||
transform.make_dynamic();
|
||||
transform.set_direction(ocio_rs::TransformDirection::Forward);
|
||||
let processor = config
|
||||
.processor_from_transform(&transform, ocio_rs::TransformDirection::Forward)
|
||||
.ok();
|
||||
let cpu = processor.as_ref().and_then(|p| p.default_cpu_processor().ok());
|
||||
Some(Self { inner: processor, cpu })
|
||||
let cpu = processor
|
||||
.as_ref()
|
||||
.and_then(|p| p.default_cpu_processor().ok());
|
||||
Some(Self {
|
||||
inner: processor,
|
||||
cpu,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create from an explicit OCIO processor (C++
|
||||
@@ -234,9 +257,7 @@ fn bytemuck_f32_slice(data: &mut [u8]) -> Option<&mut [f32]> {
|
||||
// SAFETY: length is a multiple of 4 and `data` is byte-aligned; the
|
||||
// cast is valid for any alignment (f32 has no stricter requirement
|
||||
// than u8 for mutable slice casts at this length).
|
||||
Some(unsafe {
|
||||
std::slice::from_raw_parts_mut(data.as_mut_ptr() as *mut f32, data.len() / 4)
|
||||
})
|
||||
Some(unsafe { std::slice::from_raw_parts_mut(data.as_mut_ptr() as *mut f32, data.len() / 4) })
|
||||
}
|
||||
|
||||
// ---- process-wide default config (C++ ColorManager statics) ----------------
|
||||
@@ -271,17 +292,14 @@ pub fn default_config() -> Option<std::sync::Arc<SafeConfig>> {
|
||||
/// (C++ `ColorManager::SetUpDefaultConfig`).
|
||||
pub fn set_up_default_config() -> Result<()> {
|
||||
let config = match std::env::var("OCIO") {
|
||||
Ok(path) if !path.is_empty() => {
|
||||
ocio_rs::Config::from_file(&path)
|
||||
.map_err(|e| Error::Failed(format!("load $OCIO config: {e}")))?
|
||||
}
|
||||
Ok(path) if !path.is_empty() => ocio_rs::Config::from_file(&path)
|
||||
.map_err(|e| Error::Failed(format!("load $OCIO config: {e}")))?,
|
||||
_ => ocio_rs::Config::create_from_builtin_config("default")
|
||||
.or_else(|_| ocio_rs::Config::create_from_builtin_config("ocio-2.2-default"))
|
||||
.map_err(|e| Error::Failed(format!("load bundled config: {e}")))?,
|
||||
};
|
||||
*DEFAULT_CONFIG
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner()) = Some(std::sync::Arc::new(SafeConfig(config)));
|
||||
*DEFAULT_CONFIG.lock().unwrap_or_else(|e| e.into_inner()) =
|
||||
Some(std::sync::Arc::new(SafeConfig(config)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -309,7 +327,8 @@ pub fn display_transform_result(
|
||||
}
|
||||
|
||||
let mut view_found = false;
|
||||
let views = config.num_views_by_reference_space(ocio_rs::SearchReferenceSpaceType::Scene, display);
|
||||
let views =
|
||||
config.num_views_by_reference_space(ocio_rs::SearchReferenceSpaceType::Scene, display);
|
||||
for i in 0..views {
|
||||
if config
|
||||
.view_by_reference_space(ocio_rs::SearchReferenceSpaceType::Scene, display, i)
|
||||
@@ -331,9 +350,21 @@ pub fn display_transform_result(
|
||||
.get_color_space("reference")
|
||||
.and_then(|cs| cs.name())
|
||||
.filter(|s| !s.is_empty())
|
||||
.or_else(|| config.role_color_space("reference").filter(|s| !s.is_empty()))
|
||||
.or_else(|| config.role_color_space("aces_interchange").filter(|s| !s.is_empty()))
|
||||
.or_else(|| config.role_color_space("scene_linear").filter(|s| !s.is_empty()))
|
||||
.or_else(|| {
|
||||
config
|
||||
.role_color_space("reference")
|
||||
.filter(|s| !s.is_empty())
|
||||
})
|
||||
.or_else(|| {
|
||||
config
|
||||
.role_color_space("aces_interchange")
|
||||
.filter(|s| !s.is_empty())
|
||||
})
|
||||
.or_else(|| {
|
||||
config
|
||||
.role_color_space("scene_linear")
|
||||
.filter(|s| !s.is_empty())
|
||||
})
|
||||
.ok_or(Error::State)?;
|
||||
let processor = config
|
||||
.processor_display(src, display, view, ocio_rs::TransformDirection::Forward)
|
||||
@@ -406,7 +437,10 @@ mod tests {
|
||||
fn pass_through_processor_identity() {
|
||||
let p = ColorProcessor::pass_through();
|
||||
assert!(!p.is_valid());
|
||||
assert_eq!(p.convert_color([0.25, 0.5, 0.75, 1.0]), [0.25, 0.5, 0.75, 1.0]);
|
||||
assert_eq!(
|
||||
p.convert_color([0.25, 0.5, 0.75, 1.0]),
|
||||
[0.25, 0.5, 0.75, 1.0]
|
||||
);
|
||||
let mut f = Frame::dummy();
|
||||
assert!(p.convert_frame(&mut f).is_ok());
|
||||
}
|
||||
@@ -417,7 +451,10 @@ mod tests {
|
||||
let p = ColorProcessor::pass_through();
|
||||
let mut f = Frame::new();
|
||||
f.format = PixelFormat::U8;
|
||||
assert!(p.convert_frame(&mut f).is_ok(), "pass-through converts nothing");
|
||||
assert!(
|
||||
p.convert_frame(&mut f).is_ok(),
|
||||
"pass-through converts nothing"
|
||||
);
|
||||
// A *valid* processor requires the F32 pipeline format.
|
||||
if set_up_default_config().is_err() {
|
||||
return;
|
||||
@@ -466,11 +503,17 @@ mod tests {
|
||||
// the ACES scene→display-encoded pairing and must preserve alpha.
|
||||
let p = ColorProcessor::create("ACEScg", "sRGB Encoded Rec.709 (sRGB)", Direction::Normal);
|
||||
let p = p.expect("processor handle always returned");
|
||||
assert!(p.is_valid(), "ACEScg→sRGB Encoded must be a valid processor");
|
||||
assert!(
|
||||
p.is_valid(),
|
||||
"ACEScg→sRGB Encoded must be a valid processor"
|
||||
);
|
||||
let out = p.convert_color([0.18, 0.18, 0.18, 1.0]);
|
||||
assert!((out[3] - 1.0).abs() < 1e-6, "alpha passes through");
|
||||
assert!(out[0].is_finite() && out[1].is_finite() && out[2].is_finite());
|
||||
assert!((out[0] - 0.18).abs() > 1e-3, "the transform must change the value");
|
||||
assert!(
|
||||
(out[0] - 0.18).abs() > 1e-3,
|
||||
"the transform must change the value"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -479,12 +522,8 @@ mod tests {
|
||||
if set_up_default_config().is_err() {
|
||||
return;
|
||||
}
|
||||
let p = ColorProcessor::create(
|
||||
"not_a_real_colorspace",
|
||||
"also_not_real",
|
||||
Direction::Normal,
|
||||
)
|
||||
.unwrap();
|
||||
let p = ColorProcessor::create("not_a_real_colorspace", "also_not_real", Direction::Normal)
|
||||
.unwrap();
|
||||
assert!(!p.is_valid(), "lookup failure is non-fatal (pass-through)");
|
||||
assert_eq!(
|
||||
p.convert_color([1.0, 0.5, 0.25, 0.0]),
|
||||
@@ -561,10 +600,12 @@ mod tests {
|
||||
if set_up_default_config().is_err() {
|
||||
return;
|
||||
}
|
||||
let fwd = ColorProcessor::create("ACEScg", "sRGB Encoded Rec.709 (sRGB)", Direction::Normal)
|
||||
.expect("handle");
|
||||
let inv = ColorProcessor::create("ACEScg", "sRGB Encoded Rec.709 (sRGB)", Direction::Inverse)
|
||||
.expect("handle");
|
||||
let fwd =
|
||||
ColorProcessor::create("ACEScg", "sRGB Encoded Rec.709 (sRGB)", Direction::Normal)
|
||||
.expect("handle");
|
||||
let inv =
|
||||
ColorProcessor::create("ACEScg", "sRGB Encoded Rec.709 (sRGB)", Direction::Inverse)
|
||||
.expect("handle");
|
||||
if !fwd.is_valid() || !inv.is_valid() {
|
||||
return;
|
||||
}
|
||||
@@ -590,7 +631,10 @@ mod tests {
|
||||
std::fs::write(&path, lut).unwrap();
|
||||
let p = ColorProcessor::create_lut(path.to_string_lossy().as_ref(), Direction::Normal)
|
||||
.expect("processor handle always returned");
|
||||
assert!(p.is_valid(), "a readable .cube must produce a valid processor");
|
||||
assert!(
|
||||
p.is_valid(),
|
||||
"a readable .cube must produce a valid processor"
|
||||
);
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
@@ -625,5 +669,4 @@ mod tests {
|
||||
let p = ColorProcessor::create_lut("/nonexistent/never.cube", Direction::Normal).unwrap();
|
||||
assert!(!p.is_valid(), "unreadable LUT → pass-through processor");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -152,7 +152,10 @@ mod tests {
|
||||
#[test]
|
||||
fn set_project_rejects_empty_handle() {
|
||||
let mut pc = ProjectCopy::new();
|
||||
assert_eq!(pc.set_project(ProjectHandle::null()).unwrap_err().code(), Error::Invalid.code());
|
||||
assert_eq!(
|
||||
pc.set_project(ProjectHandle::null()).unwrap_err().code(),
|
||||
Error::Invalid.code()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -88,11 +88,7 @@ impl RenderEvalHooks {
|
||||
}
|
||||
|
||||
/// C++ process_video_footage: decode + upload into `destination`.
|
||||
fn process_video_footage(
|
||||
&mut self,
|
||||
destination: &mut Texture,
|
||||
spec: &JobSpec,
|
||||
) -> Result<()> {
|
||||
fn process_video_footage(&mut self, destination: &mut Texture, spec: &JobSpec) -> Result<()> {
|
||||
let JobSpec::Footage { decoder_id } = spec else {
|
||||
return Err(Error::Invalid);
|
||||
};
|
||||
@@ -122,7 +118,11 @@ impl RenderEvalHooks {
|
||||
}
|
||||
|
||||
/// C++ process_color_transform.
|
||||
fn process_color_transform(&mut self, _destination: &mut Texture, spec: &JobSpec) -> Result<()> {
|
||||
fn process_color_transform(
|
||||
&mut self,
|
||||
_destination: &mut Texture,
|
||||
spec: &JobSpec,
|
||||
) -> Result<()> {
|
||||
let JobSpec::ColorTransform { processor } = spec else {
|
||||
return Err(Error::Invalid);
|
||||
};
|
||||
@@ -265,19 +265,29 @@ mod tests {
|
||||
}
|
||||
)
|
||||
.is_err());
|
||||
assert!(hooks.process_shader(&mut dest, &JobSpec::Shader {
|
||||
frag: "f".into(),
|
||||
vert: "v".into()
|
||||
}).is_err());
|
||||
assert!(hooks.process_video_cache_job(&JobSpec::Cache { path: "p".into() }).is_err());
|
||||
assert!(hooks
|
||||
.process_shader(
|
||||
&mut dest,
|
||||
&JobSpec::Shader {
|
||||
frag: "f".into(),
|
||||
vert: "v".into()
|
||||
}
|
||||
)
|
||||
.is_err());
|
||||
assert!(hooks
|
||||
.process_video_cache_job(&JobSpec::Cache { path: "p".into() })
|
||||
.is_err());
|
||||
assert!(hooks.process_audio_footage(&JobSpec::Sample).is_err());
|
||||
assert!(hooks.process_plugin_job(Texture::dummy(), &JobSpec::Plugin { instance: 1 }).is_err());
|
||||
assert!(hooks
|
||||
.process_plugin_job(Texture::dummy(), &JobSpec::Plugin { instance: 1 })
|
||||
.is_err());
|
||||
assert!(hooks
|
||||
.process_color_transform(&mut dest, &JobSpec::ColorTransform { processor: 1 })
|
||||
.is_err());
|
||||
// Wrong spec kinds are invalid, not deferred.
|
||||
assert_eq!(
|
||||
hooks.process_shader(&mut dest, &JobSpec::Generate)
|
||||
hooks
|
||||
.process_shader(&mut dest, &JobSpec::Generate)
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
Error::Invalid.code()
|
||||
@@ -287,11 +297,15 @@ mod tests {
|
||||
#[test]
|
||||
fn generation_fills_cpu_texture() {
|
||||
let mut hooks = RenderEvalHooks::new();
|
||||
let mut tex = Texture::wrap_frame(generate_frame(Rational::new(1, 1), (8, 8), PixelFormat::F32).unwrap());
|
||||
let mut tex = Texture::wrap_frame(
|
||||
generate_frame(Rational::new(1, 1), (8, 8), PixelFormat::F32).unwrap(),
|
||||
);
|
||||
hooks
|
||||
.process_frame_generation(&mut tex, Rational::new(3, 1))
|
||||
.unwrap();
|
||||
let Texture::Cpu(f) = &tex else { unreachable!() };
|
||||
let Texture::Cpu(f) = &tex else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(f.timestamp, Rational::new(3, 1));
|
||||
assert!(f.data.iter().all(|&b| b == 0));
|
||||
// GPU destination rejected.
|
||||
@@ -303,7 +317,9 @@ mod tests {
|
||||
format: PixelFormat::F32,
|
||||
ctx: Arc::new(UnusedCtx),
|
||||
};
|
||||
assert!(hooks.process_frame_generation(&mut gpu, Rational::new(1, 1)).is_err());
|
||||
assert!(hooks
|
||||
.process_frame_generation(&mut gpu, Rational::new(1, 1))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
/// Stand-in context for the "GPU destination" test (never used for
|
||||
|
||||
+133
-95
@@ -284,7 +284,15 @@ pub mod manager {
|
||||
|
||||
/// In-flight frame requests: request id → (arena, ticket id).
|
||||
static REQUESTS: std::sync::LazyLock<
|
||||
Mutex<std::collections::HashMap<i64, (std::sync::Arc<crate::ticket::TicketArena>, crate::ticket::TicketId)>>,
|
||||
Mutex<
|
||||
std::collections::HashMap<
|
||||
i64,
|
||||
(
|
||||
std::sync::Arc<crate::ticket::TicketArena>,
|
||||
crate::ticket::TicketId,
|
||||
),
|
||||
>,
|
||||
>,
|
||||
> = std::sync::LazyLock::new(|| Mutex::new(std::collections::HashMap::new()));
|
||||
static NEXT_REQUEST_ID: AtomicI64 = AtomicI64::new(1);
|
||||
|
||||
@@ -445,9 +453,16 @@ pub mod manager {
|
||||
crate::handle::guard(|| crate::manager::disk_cache_clear())
|
||||
}
|
||||
|
||||
fn lock(
|
||||
) -> std::sync::MutexGuard<'static, std::collections::HashMap<i64, (std::sync::Arc<crate::ticket::TicketArena>, crate::ticket::TicketId)>>
|
||||
{
|
||||
fn lock() -> std::sync::MutexGuard<
|
||||
'static,
|
||||
std::collections::HashMap<
|
||||
i64,
|
||||
(
|
||||
std::sync::Arc<crate::ticket::TicketArena>,
|
||||
crate::ticket::TicketId,
|
||||
),
|
||||
>,
|
||||
> {
|
||||
REQUESTS.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
}
|
||||
@@ -471,10 +486,12 @@ pub mod cache {
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakrender_cache_create() -> OakRenderCache {
|
||||
crate::handle::guard_handle(|| {
|
||||
Ok(crate::handle::make_owned(Some(crate::cache::PlaybackCache::new(
|
||||
crate::cache::CacheKind::VideoFrame,
|
||||
crate::cache::next_owner_identity(),
|
||||
))))
|
||||
Ok(crate::handle::make_owned(Some(
|
||||
crate::cache::PlaybackCache::new(
|
||||
crate::cache::CacheKind::VideoFrame,
|
||||
crate::cache::next_owner_identity(),
|
||||
),
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -489,7 +506,9 @@ pub mod cache {
|
||||
/// borrowed caches return `OAKRENDER_E_INVALID` until the C++ interop
|
||||
/// layer lands).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakrender_cache_wrap_borrowed(native_cache: *mut c_void) -> OakRenderCache {
|
||||
pub unsafe extern "C" fn oakrender_cache_wrap_borrowed(
|
||||
native_cache: *mut c_void,
|
||||
) -> OakRenderCache {
|
||||
if native_cache.is_null() {
|
||||
return CHandle::null();
|
||||
}
|
||||
@@ -550,9 +569,10 @@ pub mod cache {
|
||||
if context.is_null() {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let c = unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
let c =
|
||||
unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
let range = oakcore_rs::TimeRange::new(
|
||||
oakcore_rs::Rational::new(in_num, in_den),
|
||||
oakcore_rs::Rational::new(out_num, out_den),
|
||||
@@ -574,9 +594,10 @@ pub mod cache {
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakrender_cache_load_state(cache: OakRenderCache) -> c_int {
|
||||
crate::handle::guard(|| {
|
||||
let c = unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
let c =
|
||||
unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
let dir = c.disk_dir().to_string();
|
||||
c.load_state(std::path::Path::new(&dir))
|
||||
})
|
||||
@@ -600,9 +621,10 @@ pub mod cache {
|
||||
enabled: c_int,
|
||||
) -> c_int {
|
||||
crate::handle::guard(|| {
|
||||
let c = unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
let c =
|
||||
unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
c.set_saving_enabled(enabled != 0);
|
||||
Ok(())
|
||||
})
|
||||
@@ -618,9 +640,10 @@ pub mod cache {
|
||||
// Snapshot the source's data first: `cache == other` (or any
|
||||
// aliasing) must not create overlapping borrows.
|
||||
let snapshot = {
|
||||
let o = unsafe { crate::handle::get::<Option<crate::cache::PlaybackCache>>(&other) }
|
||||
.and_then(|o| o.as_ref())
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
let o =
|
||||
unsafe { crate::handle::get::<Option<crate::cache::PlaybackCache>>(&other) }
|
||||
.and_then(|o| o.as_ref())
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
crate::cache::PassthroughSnapshot {
|
||||
validated: o.validated_ranges().clone(),
|
||||
passthroughs: o.passthroughs().to_vec(),
|
||||
@@ -628,9 +651,10 @@ pub mod cache {
|
||||
uuid: o.uuid.clone(),
|
||||
}
|
||||
};
|
||||
let c = unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
let c =
|
||||
unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
c.set_passthrough_snapshot(snapshot);
|
||||
Ok(())
|
||||
})
|
||||
@@ -667,8 +691,9 @@ pub mod cache {
|
||||
ranges: *mut i64,
|
||||
max_ranges: c_int,
|
||||
) -> c_int {
|
||||
let Some(c) = (unsafe { crate::handle::get::<Option<crate::cache::PlaybackCache>>(&cache) })
|
||||
.and_then(|o| o.as_ref())
|
||||
let Some(c) =
|
||||
(unsafe { crate::handle::get::<Option<crate::cache::PlaybackCache>>(&cache) })
|
||||
.and_then(|o| o.as_ref())
|
||||
else {
|
||||
return crate::error::OAKRENDER_E_INVALID;
|
||||
};
|
||||
@@ -720,8 +745,9 @@ pub mod cache {
|
||||
/// while locked — is a documented leak, mirroring a C++ deadlock).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakrender_cache_lock(cache: OakRenderCache) {
|
||||
if let Some(c) = unsafe { crate::handle::get::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_ref())
|
||||
if let Some(c) =
|
||||
unsafe { crate::handle::get::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_ref())
|
||||
{
|
||||
// The boxed cache lives for 'static; re-derive a 'static
|
||||
// reference from the box pointer to build a 'static guard.
|
||||
@@ -741,7 +767,9 @@ pub mod cache {
|
||||
/// [`oakrender_cache_lock`].
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakrender_cache_unlock(cache: OakRenderCache) {
|
||||
if let Some(c) = unsafe { crate::handle::get::<Option<crate::cache::PlaybackCache>>(&cache) } {
|
||||
if let Some(c) =
|
||||
unsafe { crate::handle::get::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
{
|
||||
let key = c as *const _ as usize;
|
||||
if let Some(raw) = HELD_LOCKS
|
||||
.lock()
|
||||
@@ -750,7 +778,11 @@ pub mod cache {
|
||||
{
|
||||
// SAFETY: the raw address came from Box::into_raw of a
|
||||
// MutexGuard in oakrender_cache_lock.
|
||||
unsafe { drop(Box::from_raw(raw as *mut std::sync::MutexGuard<'static, ()>)) };
|
||||
unsafe {
|
||||
drop(Box::from_raw(
|
||||
raw as *mut std::sync::MutexGuard<'static, ()>,
|
||||
))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -766,9 +798,10 @@ pub mod cache {
|
||||
if num <= 0 || den <= 0 {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let c = unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
let c =
|
||||
unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
c.set_timebase(oakcore_rs::Rational::new(num as i64, den as i64));
|
||||
Ok(())
|
||||
})
|
||||
@@ -782,9 +815,10 @@ pub mod cache {
|
||||
) -> c_int {
|
||||
crate::handle::guard(|| {
|
||||
let uuid = unsafe { cstr(uuid) }.ok_or(crate::error::Error::Invalid)?;
|
||||
let c = unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
let c =
|
||||
unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
c.set_uuid(&uuid);
|
||||
Ok(())
|
||||
})
|
||||
@@ -798,8 +832,9 @@ pub mod cache {
|
||||
out_ts: i64,
|
||||
) {
|
||||
crate::handle::guard_void(|| {
|
||||
if let Some(c) = unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
if let Some(c) =
|
||||
unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
{
|
||||
let range = ts_range(c, in_ts, out_ts);
|
||||
c.invalidate(range);
|
||||
@@ -817,8 +852,9 @@ pub mod cache {
|
||||
out_den: i64,
|
||||
) {
|
||||
crate::handle::guard_void(|| {
|
||||
if let Some(c) = unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
if let Some(c) =
|
||||
unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
{
|
||||
c.invalidate(oakcore_rs::TimeRange::new(
|
||||
oakcore_rs::Rational::new(in_num, in_den),
|
||||
@@ -836,8 +872,9 @@ pub mod cache {
|
||||
out_ts: i64,
|
||||
) {
|
||||
crate::handle::guard_void(|| {
|
||||
if let Some(c) = unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
if let Some(c) =
|
||||
unsafe { crate::handle::get_mut::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_mut())
|
||||
{
|
||||
let range = ts_range(c, in_ts, out_ts);
|
||||
c.validate(range);
|
||||
@@ -847,9 +884,7 @@ pub mod cache {
|
||||
|
||||
/// `oakrender_cache_has_validated_ranges`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakrender_cache_has_validated_ranges(
|
||||
cache: OakRenderCache,
|
||||
) -> c_int {
|
||||
pub unsafe extern "C" fn oakrender_cache_has_validated_ranges(cache: OakRenderCache) -> c_int {
|
||||
match unsafe { crate::handle::get::<Option<crate::cache::PlaybackCache>>(&cache) }
|
||||
.and_then(|o| o.as_ref())
|
||||
{
|
||||
@@ -955,12 +990,8 @@ pub mod cache {
|
||||
return;
|
||||
};
|
||||
let tb = c.timebase.unwrap_or(oakcore_rs::Rational::new(1, 1));
|
||||
let filename = crate::cache::PlaybackCache::frame_cache_path(
|
||||
&path,
|
||||
&uuid,
|
||||
f.timestamp,
|
||||
tb,
|
||||
);
|
||||
let filename =
|
||||
crate::cache::PlaybackCache::frame_cache_path(&path, &uuid, f.timestamp, tb);
|
||||
eprintln!(
|
||||
"oakrender: frame_cache_save to {filename} deferred (oakcodec write bridge pending)"
|
||||
);
|
||||
@@ -1048,20 +1079,21 @@ pub mod ticket {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (cache_dir, cache_id, cache_timebase) =
|
||||
if !params.cache.is_null() {
|
||||
if let Some(c) = unsafe { crate::handle::get::<crate::cache::PlaybackCache>(¶ms.cache) } {
|
||||
(
|
||||
Some(c.disk_dir().to_string()),
|
||||
Some(c.uuid.clone()),
|
||||
c.timebase,
|
||||
)
|
||||
} else {
|
||||
(None, None, None)
|
||||
}
|
||||
let (cache_dir, cache_id, cache_timebase) = if !params.cache.is_null() {
|
||||
if let Some(c) =
|
||||
unsafe { crate::handle::get::<crate::cache::PlaybackCache>(¶ms.cache) }
|
||||
{
|
||||
(
|
||||
Some(c.disk_dir().to_string()),
|
||||
Some(c.uuid.clone()),
|
||||
c.timebase,
|
||||
)
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
|
||||
let arena = manager.tickets.clone();
|
||||
// Reserve the id up front so the handle is fully stamped before
|
||||
@@ -1185,7 +1217,10 @@ pub mod ticket {
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakrender_ticket_get_type(ticket: OakRenderTicket) -> c_int {
|
||||
match unsafe { crate::handle::get::<TicketBox>(&ticket) } {
|
||||
Some(b) => b.arena.kind(b.id).unwrap_or(crate::error::OAKRENDER_E_INVALID),
|
||||
Some(b) => b
|
||||
.arena
|
||||
.kind(b.id)
|
||||
.unwrap_or(crate::error::OAKRENDER_E_INVALID),
|
||||
None => crate::error::OAKRENDER_E_INVALID,
|
||||
}
|
||||
}
|
||||
@@ -1227,13 +1262,10 @@ pub mod ticket {
|
||||
}
|
||||
let b = unsafe { crate::handle::get::<TicketBox>(&ticket) }
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
let range = b
|
||||
.arena
|
||||
.range(b.id)
|
||||
.unwrap_or(oakcore_rs::TimeRange::new(
|
||||
oakcore_rs::Rational::NULL,
|
||||
oakcore_rs::Rational::NULL,
|
||||
));
|
||||
let range = b.arena.range(b.id).unwrap_or(oakcore_rs::TimeRange::new(
|
||||
oakcore_rs::Rational::NULL,
|
||||
oakcore_rs::Rational::NULL,
|
||||
));
|
||||
unsafe {
|
||||
*in_num = range.in_().numerator();
|
||||
*in_den = range.in_().denominator();
|
||||
@@ -1360,9 +1392,9 @@ pub mod renderer {
|
||||
"cpu" | "dummy" | "multiprocess" => crate::backend::BackendKind::Cpu,
|
||||
_ => return Err(crate::error::Error::Invalid),
|
||||
};
|
||||
Ok(crate::handle::make_owned(crate::backend::DisplayRenderer::new(
|
||||
kind,
|
||||
)))
|
||||
Ok(crate::handle::make_owned(
|
||||
crate::backend::DisplayRenderer::new(kind),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1663,7 +1695,9 @@ pub mod renderer {
|
||||
if f.allocate() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(crate::error::Error::Failed("frame allocation failed".into()))
|
||||
Err(crate::error::Error::Failed(
|
||||
"frame allocation failed".into(),
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1679,7 +1713,9 @@ pub mod renderer {
|
||||
|
||||
/// `oakrender_codec_frame_const_data`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakrender_codec_frame_const_data(frame: OakCodecFrame) -> *const c_void {
|
||||
pub unsafe extern "C" fn oakrender_codec_frame_const_data(
|
||||
frame: OakCodecFrame,
|
||||
) -> *const c_void {
|
||||
match unsafe { crate::handle::get::<crate::texture::Frame>(&frame) } {
|
||||
Some(f) if f.is_allocated() => f.data.as_ptr() as *const c_void,
|
||||
_ => std::ptr::null(),
|
||||
@@ -1800,7 +1836,11 @@ pub mod renderer {
|
||||
|
||||
/// `oakrender_backend_id_at` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakrender_backend_id_at(i: c_int, buf: *mut c_char, n: c_int) -> c_int {
|
||||
pub unsafe extern "C" fn oakrender_backend_id_at(
|
||||
i: c_int,
|
||||
buf: *mut c_char,
|
||||
n: c_int,
|
||||
) -> c_int {
|
||||
const IDS: [&str; 4] = ["opengl", "vulkan", "multiprocess", "dummy"];
|
||||
if i < 0 || i >= IDS.len() as c_int {
|
||||
return crate::error::OAKRENDER_E_NOT_FOUND;
|
||||
@@ -1851,7 +1891,6 @@ pub mod renderer {
|
||||
(*h).release = None;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1877,8 +1916,8 @@ pub mod color {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let dir = direction_kind(direction)?;
|
||||
let processor =
|
||||
crate::color::ColorProcessor::create(&src, &dst, dir).ok_or(crate::error::Error::Invalid)?;
|
||||
let processor = crate::color::ColorProcessor::create(&src, &dst, dir)
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
Ok(crate::handle::make_owned(processor))
|
||||
})
|
||||
}
|
||||
@@ -1920,12 +1959,8 @@ pub mod color {
|
||||
// Destination transform decode is a node/common bridge feature;
|
||||
// until it lands the output transform defaults to the config's
|
||||
// reference role (documented deviation).
|
||||
let processor = crate::color::ColorProcessor::create(
|
||||
&input,
|
||||
"reference",
|
||||
dir,
|
||||
)
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
let processor = crate::color::ColorProcessor::create(&input, "reference", dir)
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
Ok(crate::handle::make_owned(processor))
|
||||
})
|
||||
}
|
||||
@@ -1943,8 +1978,8 @@ pub mod color {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let dir = direction_kind(direction)?;
|
||||
let processor =
|
||||
crate::color::ColorProcessor::create_lut(&path, dir).ok_or(crate::error::Error::Invalid)?;
|
||||
let processor = crate::color::ColorProcessor::create_lut(&path, dir)
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
Ok(crate::handle::make_owned(processor))
|
||||
})
|
||||
}
|
||||
@@ -1969,7 +2004,9 @@ pub mod color {
|
||||
|
||||
/// `oakrender_lut_is_supported_extension`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakrender_lut_is_supported_extension(extension: *const c_char) -> c_int {
|
||||
pub unsafe extern "C" fn oakrender_lut_is_supported_extension(
|
||||
extension: *const c_char,
|
||||
) -> c_int {
|
||||
match unsafe { cstr(extension) } {
|
||||
Some(ext) if crate::color::is_supported_lut_extension(&ext) => 1,
|
||||
_ => 0,
|
||||
@@ -2057,7 +2094,10 @@ pub mod color {
|
||||
|
||||
/// `oakrender_color_manager_get_config` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakrender_color_manager_get_config(buf: *mut c_char, n: c_int) -> c_int {
|
||||
pub unsafe extern "C" fn oakrender_color_manager_get_config(
|
||||
buf: *mut c_char,
|
||||
n: c_int,
|
||||
) -> c_int {
|
||||
match crate::color::config_path() {
|
||||
Some(path) => guard_string(|| Ok(write_string(&path, buf, n))),
|
||||
None => crate::error::OAKRENDER_E_STATE,
|
||||
@@ -2167,11 +2207,9 @@ pub mod copier {
|
||||
) -> OakNodeProject {
|
||||
match unsafe { crate::handle::get::<crate::copier::ProjectCopy>(&copier) } {
|
||||
Some(c) => match c.copied_project() {
|
||||
Some(h) if !h.is_null() => {
|
||||
crate::handle::make_borrowed_owned(BorrowedOpaque {
|
||||
identity: h.ctx as u64,
|
||||
})
|
||||
}
|
||||
Some(h) if !h.is_null() => crate::handle::make_borrowed_owned(BorrowedOpaque {
|
||||
identity: h.ctx as u64,
|
||||
}),
|
||||
_ => CHandle::null(),
|
||||
},
|
||||
None => CHandle::null(),
|
||||
|
||||
@@ -288,7 +288,11 @@ mod tests {
|
||||
fn make_borrowed_owned_does_not_count() {
|
||||
let before = alive_count();
|
||||
let h = make_borrowed_owned(Obj(4));
|
||||
assert_eq!(alive_count(), before, "borrowed-owned boxes are not counted");
|
||||
assert_eq!(
|
||||
alive_count(),
|
||||
before,
|
||||
"borrowed-owned boxes are not counted"
|
||||
);
|
||||
assert!(!h.is_null());
|
||||
unsafe { h.release.unwrap()(h.ctx) };
|
||||
assert_eq!(alive_count(), before);
|
||||
|
||||
@@ -203,7 +203,10 @@ mod tests {
|
||||
RenderManager::init().unwrap();
|
||||
assert!(RenderManager::global().is_some());
|
||||
// Idempotence: second init is a state error.
|
||||
assert_eq!(RenderManager::init().unwrap_err().code(), Error::State.code());
|
||||
assert_eq!(
|
||||
RenderManager::init().unwrap_err().code(),
|
||||
Error::State.code()
|
||||
);
|
||||
RenderManager::shutdown();
|
||||
assert!(RenderManager::global().is_none());
|
||||
// Re-init works after shutdown (C++ destroy_instance semantics).
|
||||
@@ -238,7 +241,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn disk_cache_size_and_clear() {
|
||||
let _guard = crate::bridge::common::ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::bridge::common::ENV_TEST_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let dir = std::env::temp_dir().join("oakrender-diskcache-test");
|
||||
std::env::set_var("OAK_CONFIG_DIR", &dir);
|
||||
std::fs::create_dir_all(dir.join("mediacache").join("sub")).unwrap();
|
||||
@@ -253,7 +258,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn disk_cache_size_missing_dir_is_zero() {
|
||||
let _guard = crate::bridge::common::ENV_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::bridge::common::ENV_TEST_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let dir = std::env::temp_dir().join("oakrender-diskcache-missing");
|
||||
std::env::set_var("OAK_CONFIG_DIR", &dir);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
|
||||
@@ -340,10 +340,7 @@ impl TicketArena {
|
||||
|
||||
/// Blocking wait for completion (C++ wait_for_finished).
|
||||
pub fn wait(&self, id: TicketId) -> Result<()> {
|
||||
let slot = lock(&self.slots)
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.ok_or(Error::NotFound)?;
|
||||
let slot = lock(&self.slots).get(&id).cloned().ok_or(Error::NotFound)?;
|
||||
let mut state = lock(&slot.state);
|
||||
while !matches!(*state, SlotState::Finished) {
|
||||
state = slot.cv.wait(state).unwrap_or_else(|e| e.into_inner());
|
||||
@@ -359,9 +356,7 @@ impl TicketArena {
|
||||
|
||||
/// Ticket metadata query (C++ property()).
|
||||
pub fn meta(&self, id: TicketId) -> Option<TicketMeta> {
|
||||
lock(&self.slots)
|
||||
.get(&id)
|
||||
.map(|s| lock(&s.meta).clone())
|
||||
lock(&self.slots).get(&id).map(|s| lock(&s.meta).clone())
|
||||
}
|
||||
|
||||
/// The ticket's kind.
|
||||
@@ -454,7 +449,10 @@ mod tests {
|
||||
arena.wait(id).unwrap();
|
||||
let ok = rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
assert!(ok, "completion fired with success");
|
||||
assert!(rx.recv_timeout(Duration::from_millis(50)).is_err(), "exactly once");
|
||||
assert!(
|
||||
rx.recv_timeout(Duration::from_millis(50)).is_err(),
|
||||
"exactly once"
|
||||
);
|
||||
|
||||
let res = arena.result(id).unwrap().unwrap();
|
||||
assert_eq!(res.size(), (4, 4));
|
||||
@@ -502,7 +500,10 @@ mod tests {
|
||||
|
||||
let res = rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
assert_eq!(res.unwrap_err().code(), Error::State.code());
|
||||
assert!(rx.recv_timeout(Duration::from_millis(50)).is_err(), "exactly once");
|
||||
assert!(
|
||||
rx.recv_timeout(Duration::from_millis(50)).is_err(),
|
||||
"exactly once"
|
||||
);
|
||||
pool.shutdown();
|
||||
}
|
||||
|
||||
@@ -523,7 +524,10 @@ mod tests {
|
||||
let mut pool = pool;
|
||||
pool.start();
|
||||
let arena = TicketArena::new(pool.clone(), ok_producer());
|
||||
assert_eq!(arena.wait(TicketId(999)).unwrap_err().code(), Error::NotFound.code());
|
||||
assert_eq!(
|
||||
arena.wait(TicketId(999)).unwrap_err().code(),
|
||||
Error::NotFound.code()
|
||||
);
|
||||
pool.shutdown();
|
||||
}
|
||||
|
||||
|
||||
@@ -241,10 +241,7 @@ struct SnapshotEntry {
|
||||
impl GraphSnapshotStore {
|
||||
/// Empty store rooted in the process temp directory.
|
||||
pub fn new() -> Self {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"oakrender-snapshots-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let dir = std::env::temp_dir().join(format!("oakrender-snapshots-{}", std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
Self {
|
||||
entries: Mutex::new(HashMap::new()),
|
||||
@@ -315,10 +312,7 @@ impl GraphSnapshotStore {
|
||||
|
||||
/// Current reference count for a path (tests).
|
||||
pub fn refs(&self, path: &str) -> u64 {
|
||||
lock(&self.entries)
|
||||
.get(path)
|
||||
.map(|e| e.refs)
|
||||
.unwrap_or(0)
|
||||
lock(&self.entries).get(path).map(|e| e.refs).unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,8 +333,8 @@ pub enum WorkerBackend {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::texture::Texture;
|
||||
@@ -531,7 +525,10 @@ mod tests {
|
||||
assert_eq!(store.refs(&p1), 2);
|
||||
|
||||
store.release(&p1);
|
||||
assert!(std::path::Path::new(&p1).exists(), "refcount 1: still alive");
|
||||
assert!(
|
||||
std::path::Path::new(&p1).exists(),
|
||||
"refcount 1: still alive"
|
||||
);
|
||||
store.release(&p1);
|
||||
assert!(!std::path::Path::new(&p1).exists(), "refcount 0: unlinked");
|
||||
assert_eq!(store.refs(&p1), 0);
|
||||
|
||||
Reference in New Issue
Block a user