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:
2026-08-11 23:04:48 +08:00
parent a209f63d52
commit 18ff60f147
351 changed files with 32880 additions and 15939 deletions
+4 -1
View File
@@ -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
View File
@@ -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;
+12 -3
View File
@@ -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();
+14 -12
View File
@@ -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);
}
+9 -14
View File
@@ -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);
+83 -40
View File
@@ -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");
}
}
+4 -1
View File
@@ -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]
+32 -16
View File
@@ -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
View File
@@ -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>(&params.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>(&params.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(),
+5 -1
View File
@@ -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);
+10 -3
View File
@@ -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);
+14 -10
View File
@@ -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();
}
+7 -10
View File
@@ -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);