clippy: clear the workspace errors and apply the machine fixes

- Mark the raw-pointer interop entry points unsafe with # Safety docs
  (oak-core upload/download/frame-from-pixels, oak-audio convert) and
  satisfy the existing callers (tests).
- mut_from_ref: allow with the ABI contract documented (the handle
  get_mut helpers in oak-timeline/oak-render/oak-task take the shared
  reference the C ABI passes; exclusivity is the caller's unsafe
  contract).
- Fix the eq_op in the white-balance normalization (green / green).
- Apply cargo clippy --fix across the workspace (redundant closures and
  field names, field reassignment, items after test modules, ...).
- Revert the replace_box fix in image_effect's clip_define: a
  redefinition must allocate a new box, otherwise the old clip handle
  stays valid and the HS-map replace contract (clip != clip2) breaks.
- 283 warnings remain; they are all non-machine-applicable
  (chunks_exact -> as_chunks needs a manual iter_mut, too_many_arguments,
  complex types, missing Safety docs, ...) and are tracked as the
  follow-up.
This commit is contained in:
2026-09-15 19:29:32 +08:00
parent 7e87ec135e
commit ee7ea18d94
146 changed files with 5235 additions and 4046 deletions
+162 -95
View File
@@ -40,9 +40,9 @@ use std::sync::{Arc, Mutex, MutexGuard};
use crate::PixelFormat;
use crate::error::{Error, Result};
use crate::frame::VideoParamsPod;
use crate::texture::{Frame, Texture};
use crate::error::{Error, Result};
/// Backend selection preference (mapped onto wgpu backends).
///
@@ -183,9 +183,8 @@ impl DisplayBitDepth {
/// Read the user's persisted choice through the oak_core config C ABI
/// ("DisplayBitDepth").
pub fn from_user_config() -> DisplayBitDepth {
let configured =
crate::commonutil::config_get_string(None, CONFIG_KEY_DISPLAY_BIT_DEPTH)
.unwrap_or_default();
let configured = crate::commonutil::config_get_string(None, CONFIG_KEY_DISPLAY_BIT_DEPTH)
.unwrap_or_default();
DisplayBitDepth::from_config_string(&configured)
}
@@ -243,10 +242,10 @@ pub trait GpuContextLike: Send + Sync {
fn download(&self, token: u64) -> Result<Frame>;
/// Blit texture → texture (plain copy; color-managed deferred).
fn blit(
&self,
src: u64,
dst: u64,
processor: Option<&crate::color::ColorProcessor>,
&self,
src: u64,
dst: u64,
processor: Option<&crate::color::ColorProcessor>,
) -> Result<()>;
/// Concrete-context downcast hook (M2). The present path uses it to
@@ -432,7 +431,9 @@ impl GpuContext {
// (widely available on desktop GPUs); without it effect
// shaders sample nearest — a quality degradation, not a
// failure (logged once by the shaderfx runner).
let filterable = adapter.features().contains(wgpu::Features::FLOAT32_FILTERABLE);
let filterable = adapter
.features()
.contains(wgpu::Features::FLOAT32_FILTERABLE);
let mut required_features = wgpu::Features::empty();
if filterable {
required_features |= wgpu::Features::FLOAT32_FILTERABLE;
@@ -789,12 +790,7 @@ impl GpuContext {
/// nodes (`ColorTransformJob`, M2). The LUT texture is uploaded once
/// per key (the caller passes a stable processor cache id); applying
/// it is GPU→GPU.
pub fn apply_color_lut(
&self,
src: u64,
key: &str,
lut: &crate::lut::Lut3d,
) -> Result<u64> {
pub fn apply_color_lut(&self, src: u64, key: &str, lut: &crate::lut::Lut3d) -> Result<u64> {
let token = {
let mut cache = lock(&self.color_luts);
if let Some((_, token)) = cache.iter().find(|(k, _)| k == key) {
@@ -873,7 +869,8 @@ impl GpuContext {
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.queue.write_buffer(&uniform, 0, &f32_uniform_bytes(&params));
self.queue
.write_buffer(&uniform, 0, &f32_uniform_bytes(&params));
let src_view = src_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
@@ -1023,12 +1020,11 @@ impl GpuContext {
cache: None,
});
if let Some(err) = pollster_block_on(scope.pop()) {
return Err(Error::Failed(format!("present pipeline validation failed: {err}")));
return Err(Error::Failed(format!(
"present pipeline validation failed: {err}"
)));
}
let program = PresentPipeline {
pipeline,
layout,
};
let program = PresentPipeline { pipeline, layout };
cache.push((format, program.clone()));
Ok(program)
}
@@ -1090,7 +1086,8 @@ impl GpuContext {
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.queue.write_buffer(&uniform, 0, &f32_uniform_bytes(&params));
self.queue
.write_buffer(&uniform, 0, &f32_uniform_bytes(&params));
let y_view = y_tex
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
@@ -1239,12 +1236,11 @@ impl GpuContext {
cache: None,
});
if let Some(err) = pollster_block_on(scope.pop()) {
return Err(Error::Failed(format!("YUV pipeline validation failed: {err}")));
return Err(Error::Failed(format!(
"YUV pipeline validation failed: {err}"
)));
}
let program = PresentPipeline {
pipeline,
layout,
};
let program = PresentPipeline { pipeline, layout };
*cache = Some(program.clone());
Ok(program)
}
@@ -1438,10 +1434,10 @@ impl GpuContext {
/// of this pass (see README §4), so a `Some` processor returns
/// `Error::Failed` and the plain-copy WGSL pipeline is used for `None`.
pub fn blit(
&self,
src: u64,
dst: u64,
processor: Option<&crate::color::ColorProcessor>,
&self,
src: u64,
dst: u64,
processor: Option<&crate::color::ColorProcessor>,
) -> Result<()> {
if processor.is_some() {
return Err(Error::Failed(
@@ -1767,7 +1763,9 @@ impl GpuContext {
cache: None,
});
if let Some(err) = pollster_block_on(scope.pop()) {
return Err(Error::Failed(format!("effect pipeline validation failed: {err}")));
return Err(Error::Failed(format!(
"effect pipeline validation failed: {err}"
)));
}
let program = Arc::new(ShaderProgram {
@@ -2121,7 +2119,6 @@ fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {
}
"#;
impl GpuContextLike for GpuContext {
fn kind(&self) -> BackendKind {
self.kind()
@@ -2140,10 +2137,10 @@ impl GpuContextLike for GpuContext {
}
fn blit(
&self,
src: u64,
dst: u64,
processor: Option<&crate::color::ColorProcessor>,
&self,
src: u64,
dst: u64,
processor: Option<&crate::color::ColorProcessor>,
) -> Result<()> {
self.blit(src, dst, processor)
}
@@ -2197,11 +2194,11 @@ fn pollster_block_on<F: std::future::Future>(future: F) -> F::Output {
// Minimal futures executor (wgpu brings futures-core transitively; a tiny
// block_on is enough for the immediately-ready adapter/device futures).
mod futures_executor {
use std::future::Future;
use std::pin::pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use std::future::Future;
use std::pin::pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
fn noop_raw_waker() -> RawWaker {
fn noop_raw_waker() -> RawWaker {
fn no_op(_: *const ()) {}
fn clone(_: *const ()) -> RawWaker {
noop_raw_waker()
@@ -2353,7 +2350,13 @@ impl DisplayRenderer {
}
/// Upload pixels into a texture (GPU: backend upload; CPU: buffer copy).
pub fn upload_texture(
///
/// # Safety
///
/// `pixels` must point to at least `linesize * height` readable bytes
/// of the texture's pixel format, and `linesize` must equal the
/// frame's line size.
pub unsafe fn upload_texture(
&self,
texture: &mut Texture,
pixels: *const u8,
@@ -2362,7 +2365,7 @@ impl DisplayRenderer {
let size = texture.size();
match texture {
Texture::Gpu { token, ctx, .. } => {
let frame = frame_from_pixels_for_upload(size, pixels, linesize)?;
let frame = unsafe { frame_from_pixels_for_upload(size, pixels, linesize) }?;
ctx.upload(*token, &frame)
}
Texture::Cpu(frame) => {
@@ -2380,7 +2383,17 @@ impl DisplayRenderer {
}
/// Download a texture's pixels into `dst` (with `linesize` stride).
pub fn download_texture(&self, texture: &Texture, dst: *mut u8, linesize: usize) -> Result<()> {
///
/// # Safety
///
/// `dst` must point to at least `linesize * height` writable bytes,
/// and `linesize` must equal the frame's line size.
pub unsafe fn download_texture(
&self,
texture: &Texture,
dst: *mut u8,
linesize: usize,
) -> Result<()> {
let frame = texture.to_frame()?;
let stride = frame.linesize_bytes();
if linesize != stride {
@@ -2397,10 +2410,10 @@ impl DisplayRenderer {
/// GPU path: plain-copy WGSL blit; a color processor on the GPU path is
/// deferred (`Error::Failed`, see [`GpuContext::blit`]).
pub fn blit_color_managed(
&self,
src: Option<&Texture>,
dst: &mut Texture,
processor: Option<&crate::color::ColorProcessor>,
&self,
src: Option<&Texture>,
dst: &mut Texture,
processor: Option<&crate::color::ColorProcessor>,
) -> Result<()> {
match (src, dst) {
(
@@ -2430,7 +2443,12 @@ impl DisplayRenderer {
/// Cross-backend texture download by id (GPU registry only; CPU
/// textures have no id registry — documented).
pub fn download_from_texture(
///
/// # Safety
///
/// `dst` must point to at least `linesize * height` writable bytes,
/// and `linesize` must equal the F32 line size.
pub unsafe fn download_from_texture(
&self,
texture_id: i32,
params: &VideoParamsPod,
@@ -2464,7 +2482,12 @@ pub fn texture_id_of(t: &Texture) -> i32 {
}
/// Build an F32 frame from raw pixels (for GPU upload).
pub fn frame_from_pixels_for_upload(
///
/// # Safety
///
/// `pixels` must point to `linesize * height` readable bytes of F32 RGBA
/// data (a null pointer is rejected with `Error::Invalid`).
pub unsafe fn frame_from_pixels_for_upload(
size: (i32, i32),
pixels: *const u8,
linesize: usize,
@@ -2489,14 +2512,14 @@ pub fn frame_from_pixels_for_upload(
#[cfg(test)]
mod tests {
use super::*;
use super::*;
/// Serializes the tests that assert on the process-global GPU
/// transfer counters (and install a display LUT): parallel tests would
/// otherwise see each other's transfers.
static GPU_COUNTER_LOCK: Mutex<()> = Mutex::new(());
/// Serializes the tests that assert on the process-global GPU
/// transfer counters (and install a display LUT): parallel tests would
/// otherwise see each other's transfers.
static GPU_COUNTER_LOCK: Mutex<()> = Mutex::new(());
#[test]
#[test]
fn backend_string_roundtrip() {
for (s, kind) in [
("auto", BackendKind::Auto),
@@ -2534,13 +2557,19 @@ mod tests {
#[test]
fn display_bit_depth_string_roundtrip() {
assert_eq!(DisplayBitDepth::from_config_string("8"), DisplayBitDepth::Bit8);
assert_eq!(
DisplayBitDepth::from_config_string("8"),
DisplayBitDepth::Bit8
);
assert_eq!(
DisplayBitDepth::from_config_string("10"),
DisplayBitDepth::Bit10
);
// Unknown / missing → the 10-bit default.
assert_eq!(DisplayBitDepth::from_config_string(""), DisplayBitDepth::Bit10);
assert_eq!(
DisplayBitDepth::from_config_string(""),
DisplayBitDepth::Bit10
);
assert_eq!(
DisplayBitDepth::from_config_string("bogus"),
DisplayBitDepth::Bit10
@@ -2560,12 +2589,18 @@ mod tests {
// 10-bit → the 10-bit (RGB10A2) swapchain format, HDR float fallback.
assert_eq!(
DisplayBitDepth::Bit10.present_formats(),
&[wgpu::TextureFormat::Rgb10a2Unorm, wgpu::TextureFormat::Rgba16Float]
&[
wgpu::TextureFormat::Rgb10a2Unorm,
wgpu::TextureFormat::Rgba16Float
]
);
// 8-bit → the engine's current default preference list.
assert_eq!(
DisplayBitDepth::Bit8.present_formats(),
&[wgpu::TextureFormat::Bgra8Unorm, wgpu::TextureFormat::Rgba8Unorm]
&[
wgpu::TextureFormat::Bgra8Unorm,
wgpu::TextureFormat::Rgba8Unorm
]
);
}
@@ -2590,8 +2625,7 @@ mod tests {
assert!(!GpuContext::shared().unwrap().is_adopted());
let (device, queue) = base.device_queue();
let adopted =
GpuContext::adopt(device, queue, BackendKind::Auto);
let adopted = GpuContext::adopt(device, queue, BackendKind::Auto);
assert!(
GpuContext::install_shared(Some(adopted.clone())),
"an unused engine context is replaceable by the UI device"
@@ -2605,8 +2639,7 @@ mod tests {
// pulled out from under in-flight work.
let _texture = adopted.create_texture(2, 2).unwrap();
let (device, queue) = adopted.device_queue();
let second =
GpuContext::adopt(device, queue, BackendKind::Auto);
let second = GpuContext::adopt(device, queue, BackendKind::Auto);
assert!(
!GpuContext::install_shared(Some(second)),
"a used context is not replaceable"
@@ -2728,8 +2761,7 @@ mod tests {
for i in 0..(w * h) as usize {
let px = sample(i);
for (c, v) in px.iter().enumerate() {
frame.data[i * 16 + c * 4..i * 16 + c * 4 + 4]
.copy_from_slice(&v.to_le_bytes());
frame.data[i * 16 + c * 4..i * 16 + c * 4 + 4].copy_from_slice(&v.to_le_bytes());
}
}
ctx.upload(src, &frame).unwrap();
@@ -2859,10 +2891,10 @@ mod tests {
let mut expected = vec![0.0f32; w * h * 4];
let mut compare = |ctx: &GpuContext,
tag: &str,
matrix: crate::colormath::YuvMatrix,
transform: YuvTransform,
full_range: bool| {
tag: &str,
matrix: crate::colormath::YuvMatrix,
transform: YuvTransform,
full_range: bool| {
crate::colormath::yuv444p16_to_rgb_f32(
&y_code_plane,
w * 2,
@@ -2893,12 +2925,48 @@ mod tests {
}
};
use crate::colormath::YuvMatrix;
compare(&ctx, "bt601 limited", YuvMatrix::Bt601, YuvTransform::bt601_limited(), false);
compare(&ctx, "bt601 full", YuvMatrix::Bt601, YuvTransform::bt601_full(), true);
compare(&ctx, "bt709 limited", YuvMatrix::Bt709, YuvTransform::bt709_limited(), false);
compare(&ctx, "bt709 full", YuvMatrix::Bt709, YuvTransform::bt709_full(), true);
compare(&ctx, "bt2020 limited", YuvMatrix::Bt2020, YuvTransform::bt2020_limited(), false);
compare(&ctx, "bt2020 full", YuvMatrix::Bt2020, YuvTransform::bt2020_full(), true);
compare(
&ctx,
"bt601 limited",
YuvMatrix::Bt601,
YuvTransform::bt601_limited(),
false,
);
compare(
&ctx,
"bt601 full",
YuvMatrix::Bt601,
YuvTransform::bt601_full(),
true,
);
compare(
&ctx,
"bt709 limited",
YuvMatrix::Bt709,
YuvTransform::bt709_limited(),
false,
);
compare(
&ctx,
"bt709 full",
YuvMatrix::Bt709,
YuvTransform::bt709_full(),
true,
);
compare(
&ctx,
"bt2020 limited",
YuvMatrix::Bt2020,
YuvTransform::bt2020_limited(),
false,
);
compare(
&ctx,
"bt2020 full",
YuvMatrix::Bt2020,
YuvTransform::bt2020_full(),
true,
);
ctx.destroy_texture(y);
ctx.destroy_texture(u);
@@ -2914,9 +2982,7 @@ mod tests {
let Some(ctx) = any_gpu() else {
return;
};
let lut = crate::lut::Lut3d::build(9, [0.0; 3], [1.0; 3], |c| {
[c[1], c[2], c[0]]
});
let lut = crate::lut::Lut3d::build(9, [0.0; 3], [1.0; 3], |c| [c[1], c[2], c[0]]);
let (w, h) = (4, 2);
let src = ctx.create_texture(w, h).unwrap();
let mut frame = Frame::new();
@@ -2933,8 +2999,7 @@ mod tests {
1.0,
];
for (c, v) in px.iter().enumerate() {
frame.data[i * 16 + c * 4..i * 16 + c * 4 + 4]
.copy_from_slice(&v.to_le_bytes());
frame.data[i * 16 + c * 4..i * 16 + c * 4 + 4].copy_from_slice(&v.to_le_bytes());
}
}
ctx.upload(src, &frame).unwrap();
@@ -2960,7 +3025,9 @@ mod tests {
let want = lut.eval(src_px);
for c in 0..3 {
let g = f32::from_le_bytes(
got.data[i * 16 + c * 4..i * 16 + c * 4 + 4].try_into().unwrap(),
got.data[i * 16 + c * 4..i * 16 + c * 4 + 4]
.try_into()
.unwrap(),
);
assert!(
(g - want[c]).abs() < 1e-4,
@@ -3008,7 +3075,7 @@ mod tests {
#[test]
fn display_renderer_rejects_foreign_gl_context() {
let mut r = DisplayRenderer::new(BackendKind::Gl);
let fake = 0x1 as *mut std::ffi::c_void;
let fake = std::ptr::dangling_mut::<std::ffi::c_void>();
assert_eq!(r.init(fake).unwrap_err().code(), Error::Invalid.code());
}
@@ -3039,20 +3106,20 @@ 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(),
)
unsafe {
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())
unsafe { r3.download_texture(&upload_target, buf.as_mut_ptr(), frame.linesize_bytes()) }
.unwrap();
assert_eq!(buf[0], 0x77);
// Stride mismatch rejected.
assert!(r3
.upload_texture(&mut upload_target, frame.data.as_ptr(), 1)
.is_err());
assert!(unsafe { r3.upload_texture(&mut upload_target, frame.data.as_ptr(), 1) }.is_err());
}
#[test]
@@ -3077,9 +3144,9 @@ mod tests {
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()),
Some(&src),
&mut dst,
Some(&crate::color::ColorProcessor::pass_through()),
)
.unwrap();
// Size mismatch rejected.
+40 -25
View File
@@ -318,7 +318,8 @@ impl ColorProcessor {
}
/// Convert a whole F32 frame in place (row-major RGBA).
pub fn convert_frame(&self, frame: &mut Frame) -> Result<()> { let Some(cpu) = &self.cpu else {
pub fn convert_frame(&self, frame: &mut Frame) -> Result<()> {
let Some(cpu) = &self.cpu else {
return Ok(()); // pass-through
};
if frame.format != PixelFormat::F32 {
@@ -366,8 +367,7 @@ impl ColorProcessor {
if data.len() < count {
return Err(Error::Invalid);
}
let mut f32s: Vec<f32> =
data[..count].iter().map(|&v| v as f32 / 255.0).collect();
let mut f32s: Vec<f32> = data[..count].iter().map(|&v| v as f32 / 255.0).collect();
cpu.try_apply_rgba_pixels(&mut f32s, pixels, 4)
.map_err(|e| Error::Failed(format!("OCIO f32 apply (bgra8 detour): {e}")))?;
for (dst, v) in data[..count].iter_mut().zip(f32s) {
@@ -409,7 +409,7 @@ impl GradingStyle {
/// Reinterpret a byte buffer as f32 when its length is a multiple of 4
/// (the pipeline guarantees F32 RGBA frames, so alignment is exact).
fn bytemuck_f32_slice(data: &mut [u8]) -> Option<&mut [f32]> {
if data.len() % 4 != 0 {
if !data.len().is_multiple_of(4) {
return None;
}
// SAFETY: length is a multiple of 4 and `data` is byte-aligned; the
@@ -427,11 +427,17 @@ fn bytemuck_f32_slice(data: &mut [u8]) -> Option<&mut [f32]> {
/// project-properties commit). Render and export paths read it — a single
/// project is open at a time, so a process global is the same shape as the
/// OCIO default config above.
static PIPELINE_COLOR: LazyLock<Mutex<(crate::colormath::WorkingColorSpace, crate::colormath::OutputColorSpec)>> =
LazyLock::new(|| Mutex::new((
static PIPELINE_COLOR: LazyLock<
Mutex<(
crate::colormath::WorkingColorSpace,
crate::colormath::OutputColorSpec,
)>,
> = LazyLock::new(|| {
Mutex::new((
crate::colormath::WorkingColorSpace::default(),
crate::colormath::OutputColorSpec::default(),
)));
))
});
/// Set the pipeline color settings (working space + output spec).
pub fn set_pipeline_color_settings(
@@ -594,9 +600,7 @@ pub fn config_path() -> Option<String> {
return Some(path);
}
}
if default_config().is_none() {
return None;
}
default_config()?;
Some(format!(
"{}/ocioconf/config.ocio",
crate::commonutil::configuration_location()
@@ -836,8 +840,8 @@ mod tests {
let _lock = config_lock();
if set_up_default_config().is_err() {
return;
} // Create via the built-in config; a valid processor must exist for
// the ACES scene→display-encoded pairing and must preserve alpha.
} // Create via the built-in config; a valid processor must exist for
// 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!(
@@ -879,7 +883,8 @@ mod tests {
return;
}
let display = config.display_all(0).unwrap();
let n = config.num_views_by_reference_space(ocio_rs::SearchReferenceSpaceType::Scene, &display);
let n =
config.num_views_by_reference_space(ocio_rs::SearchReferenceSpaceType::Scene, &display);
assert!(n >= 0);
if n > 0 {
let view = config
@@ -913,7 +918,7 @@ mod tests {
let f32s: &mut [f32] = unsafe {
std::slice::from_raw_parts_mut(f.data.as_mut_ptr() as *mut f32, f.pixel_count() * 4)
};
for px in f32s.chunks_exact_mut(4) {
for px in f32s.as_chunks_mut::<4>().0 {
px[0] = 0.18;
px[1] = 0.18;
px[2] = 0.18;
@@ -960,8 +965,10 @@ mod tests {
"0.18 linear grey should land near 0.5 sRGB (got {})",
out[0]
);
assert!((out[0] - out[1]).abs() < 1e-3 && (out[1] - out[2]).abs() < 1e-3,
"grey stays grey: {out:?}");
assert!(
(out[0] - out[1]).abs() < 1e-3 && (out[1] - out[2]).abs() < 1e-3,
"grey stays grey: {out:?}"
);
assert!((out[3] - 1.0).abs() < 1e-5, "alpha preserved");
}
@@ -1073,7 +1080,7 @@ mod tests {
let f32_result = p32.convert_f32_rgba(&mut f32s, 4);
eprintln!("f32 convert: {f32_result:?} -> {f32s:?}");
bgra_result.expect("convert");
for (i, px) in data.chunks_exact(4).enumerate() {
for (i, px) in data.as_chunks::<4>().0.iter().enumerate() {
assert_eq!(px[3], 255, "pixel {i}: alpha preserved");
let rgb: u32 = px[0] as u32 + px[1] as u32 + px[2] as u32;
assert!(rgb > 0, "pixel {i} must not be crushed to black: {px:?}");
@@ -1085,7 +1092,7 @@ mod tests {
"grey stays grey through the display ICC: {b} {g} {r}"
);
f32_result.expect("convert f32");
for (i, px) in f32s.chunks_exact(4).enumerate() {
for (i, px) in f32s.as_chunks::<4>().0.iter().enumerate() {
assert!((px[3] - 1.0).abs() < 1e-3, "pixel {i}: alpha preserved");
let rgb = px[0] + px[1] + px[2];
assert!(rgb > 0.0, "pixel {i} must not be crushed to black: {px:?}");
@@ -1171,7 +1178,10 @@ mod tests {
stub.contains("ove_grading_primary"),
"function name present"
);
assert!(!stub.contains("sampler"), "no LUT upload expected for grading");
assert!(
!stub.contains("sampler"),
"no LUT upload expected for grading"
);
// Cache hit: a repeated call returns the same text.
let again = grading_primary_function_shader(style).unwrap();
assert_eq!(stub, again);
@@ -1200,8 +1210,14 @@ mod tests {
"cie_xyz_d65_interchange",
)
.expect("default config generates an analytic shader");
assert!(stub.contains("SceneLinearToCIEXYZ_d65"), "function name present");
assert!(!stub.contains("sampler"), "no LUT upload expected in the default config");
assert!(
stub.contains("SceneLinearToCIEXYZ_d65"),
"function name present"
);
assert!(
!stub.contains("sampler"),
"no LUT upload expected in the default config"
);
// Cache hit: a repeated call returns the same text.
let again = ocio_function_shader(
"SceneLinearToCIEXYZ_d65",
@@ -1217,9 +1233,9 @@ mod tests {
let _lock = config_lock();
// studio-config's Rec.709 display is a CLF LUT chain (unlike the
// analytic sRGB one); without a LUT upload path it must be refused.
let Ok(cfg) = ocio_rs::Config::create_from_builtin_config(
"studio-config-v2.1.0_aces-v1.3_ocio-v2.3",
) else {
let Ok(cfg) =
ocio_rs::Config::create_from_builtin_config("studio-config-v2.1.0_aces-v1.3_ocio-v2.3")
else {
return;
};
let cfg = SafeConfig(cfg);
@@ -1234,4 +1250,3 @@ mod tests {
);
}
}
+217 -64
View File
@@ -495,15 +495,14 @@ const LUT_N: usize = 4096;
/// Declare one lazily-built transfer LUT over [0, 1].
macro_rules! tf_lut {
($name:ident, $f:expr) => {
static $name: std::sync::LazyLock<[f32; LUT_N + 1]> =
std::sync::LazyLock::new(|| {
let f: fn(f32) -> f32 = $f;
let mut t = [0.0f32; LUT_N + 1];
for (i, e) in t.iter_mut().enumerate() {
*e = f(i as f32 / LUT_N as f32);
}
t
});
static $name: std::sync::LazyLock<[f32; LUT_N + 1]> = std::sync::LazyLock::new(|| {
let f: fn(f32) -> f32 = $f;
let mut t = [0.0f32; LUT_N + 1];
for (i, e) in t.iter_mut().enumerate() {
*e = f(i as f32 / LUT_N as f32);
}
t
});
};
}
@@ -663,8 +662,8 @@ pub enum SourceTransfer {
/// (AVCOL_PRI_* numbering, H.273 ISO codes.)
pub fn source_primaries_from_av(color_primaries: i32) -> SourcePrimaries {
match color_primaries {
1 => SourcePrimaries::Bt709, // BT.709
9 => SourcePrimaries::Bt2020, // BT.2020
1 => SourcePrimaries::Bt709, // BT.709
9 => SourcePrimaries::Bt2020, // BT.2020
11 => SourcePrimaries::DisplayP3, // SMPTE RP 431-2 (DCI-P3)
12 => SourcePrimaries::DisplayP3, // SMPTE EG 432-1 (Display P3)
_ => SourcePrimaries::Unknown,
@@ -675,16 +674,16 @@ pub fn source_primaries_from_av(color_primaries: i32) -> SourcePrimaries {
/// (AVCOL_TRC_* numbering, H.273 ISO codes.)
pub fn source_transfer_from_av(color_trc: i32) -> SourceTransfer {
match color_trc {
1 => SourceTransfer::SdrGamma, // BT.709 (see the enum docs: OETF inverse ≈ 2.2)
4 => SourceTransfer::Gamma22, // gamma 2.2
5 => SourceTransfer::Gamma28, // gamma 2.8
6 => SourceTransfer::SdrGamma, // SMPTE 170M
1 => SourceTransfer::SdrGamma, // BT.709 (see the enum docs: OETF inverse ≈ 2.2)
4 => SourceTransfer::Gamma22, // gamma 2.2
5 => SourceTransfer::Gamma28, // gamma 2.8
6 => SourceTransfer::SdrGamma, // SMPTE 170M
13 => SourceTransfer::SdrGamma, // sRGB (its EOTF is applied at decode)
14 => SourceTransfer::SdrGamma, // BT.2020 10-bit
15 => SourceTransfer::SdrGamma, // BT.2020 12-bit
16 => SourceTransfer::Pq, // SMPTE ST 2084
18 => SourceTransfer::Hlg, // ARIB STD-B67
8 => SourceTransfer::Linear, // linear
16 => SourceTransfer::Pq, // SMPTE ST 2084
18 => SourceTransfer::Hlg, // ARIB STD-B67
8 => SourceTransfer::Linear, // linear
_ => SourceTransfer::Unknown,
}
}
@@ -708,15 +707,11 @@ impl SourcePrimaries {
/// own primaries) → ACEScg linear. `samples` is an F32 RGBA buffer,
/// transformed in place. Linearize and the primaries matrix are fused
/// into a single pass (LUT'd transfer, row-band parallel).
pub fn decode_to_acescg(
samples: &mut [f32],
primaries: SourcePrimaries,
transfer: SourceTransfer,
) {
pub fn decode_to_acescg(samples: &mut [f32], primaries: SourcePrimaries, transfer: SourceTransfer) {
let linearize = decode_transfer_fn(transfer);
let matrix = rgb_to_rgb_matrix(primaries.primaries(), PRIMARIES_AP1);
par_pixels_f32(samples, move |band| {
for px in band.chunks_exact_mut(4) {
for px in band.as_chunks_mut::<4>().0 {
for c in 0..3 {
px[c] = linearize(px[c]);
}
@@ -740,7 +735,7 @@ pub fn decode_to_acescg_bytes(
let linearize = decode_transfer_fn(transfer);
let matrix = rgb_to_rgb_matrix(primaries.primaries(), PRIMARIES_AP1);
par_pixels_bytes(bytes, move |band| {
for px in band.chunks_exact_mut(16) {
for px in band.as_chunks_mut::<16>().0 {
let mut v = [
f32::from_le_bytes(px[0..4].try_into().unwrap()),
f32::from_le_bytes(px[4..8].try_into().unwrap()),
@@ -772,7 +767,7 @@ pub fn acescg_to_output(samples: &mut [f32], spec: OutputColorSpec) {
let matrix = rgb_to_rgb_matrix(PRIMARIES_AP1, spec.gamut.primaries());
let encode = output_transfer_fn(spec.transfer);
par_pixels_f32(samples, move |band| {
for px in band.chunks_exact_mut(4) {
for px in band.as_chunks_mut::<4>().0 {
apply_mat(matrix, px);
for c in 0..3 {
px[c] = encode(px[c].clamp(0.0, 1.0));
@@ -792,7 +787,7 @@ pub fn acescg_to_output_bytes(bytes: &mut [u8], pixels: usize, spec: OutputColor
let matrix = rgb_to_rgb_matrix(PRIMARIES_AP1, spec.gamut.primaries());
let encode = output_transfer_fn(spec.transfer);
par_pixels_bytes(bytes, move |band| {
for px in band.chunks_exact_mut(16) {
for px in band.as_chunks_mut::<16>().0 {
let mut v = [
f32::from_le_bytes(px[0..4].try_into().unwrap()),
f32::from_le_bytes(px[4..8].try_into().unwrap()),
@@ -816,7 +811,7 @@ pub fn acescg_to_output_bytes(bytes: &mut [u8], pixels: usize, spec: OutputColor
pub fn apply_transfer_oetf(samples: &mut [f32], transfer: OutputTransfer) {
let encode = output_transfer_fn(transfer);
par_pixels_f32(samples, move |band| {
for px in band.chunks_exact_mut(4) {
for px in band.as_chunks_mut::<4>().0 {
for c in 0..3 {
px[c] = encode(px[c]);
}
@@ -860,7 +855,7 @@ pub fn output_spec_to_xyz_d65(samples: &mut [f32], spec: OutputColorSpec) {
});
let matrix = rgb_to_xyz_matrix(spec.gamut.primaries());
par_pixels_f32(samples, move |band| {
for px in band.chunks_exact_mut(4) {
for px in band.as_chunks_mut::<4>().0 {
for c in 0..3 {
px[c] = linearize(px[c]);
}
@@ -931,9 +926,18 @@ pub fn yuv444p16_to_rgb_f32(
// `width` samples.
let row_bytes = width.saturating_mul(2);
let rows_after = height.saturating_sub(1);
if y_plane.len() < rows_after.saturating_mul(y_stride).saturating_add(row_bytes)
|| u_plane.len() < rows_after.saturating_mul(u_stride).saturating_add(row_bytes)
|| v_plane.len() < rows_after.saturating_mul(v_stride).saturating_add(row_bytes)
if y_plane.len()
< rows_after
.saturating_mul(y_stride)
.saturating_add(row_bytes)
|| u_plane.len()
< rows_after
.saturating_mul(u_stride)
.saturating_add(row_bytes)
|| v_plane.len()
< rows_after
.saturating_mul(v_stride)
.saturating_add(row_bytes)
{
return;
}
@@ -946,7 +950,11 @@ pub fn yuv444p16_to_rgb_f32(
} else {
(4096.0f32, 32768.0f32, 1.0 / 57344.0)
};
let y_scale = if full_range { 1.0 / 65535.0 } else { 1.0 / 56064.0 };
let y_scale = if full_range {
1.0 / 65535.0
} else {
1.0 / 56064.0
};
for row in 0..height {
let y_row = row * y_stride;
let u_row = row * u_stride;
@@ -1002,9 +1010,9 @@ mod tests {
// 0.0206157712 0.1095697056 0.8698145232
let m = rgb_to_rgb_matrix(PRIMARIES_SRGB, PRIMARIES_AP1);
let expected: Mat3 = [
[0.6131324224, 0.3395230762, 0.0473445014],
[0.0701922769, 0.9163536767, 0.0134540464],
[0.0206157712, 0.1095697056, 0.8698145232],
[0.613_132_4, 0.339_523_08, 0.047_344_502],
[0.070_192_28, 0.916_353_7, 0.013_454_046],
[0.020_615_771, 0.109_569_706, 0.869_814_5],
];
for r in 0..3 {
for c in 0..3 {
@@ -1034,7 +1042,11 @@ mod tests {
// → sRGB linear → (OETF) → the original codes.
let original = [0.25f32, 0.5, 0.75, 1.0];
let mut samples = original;
decode_to_acescg(&mut samples, SourcePrimaries::Bt709, SourceTransfer::SdrGamma);
decode_to_acescg(
&mut samples,
SourcePrimaries::Bt709,
SourceTransfer::SdrGamma,
);
acescg_to_output(
&mut samples,
OutputColorSpec {
@@ -1056,7 +1068,11 @@ mod tests {
fn round_trip_bt2020_acescg_bt2020() {
let original = [0.2f32, 0.6, 0.9, 1.0];
let mut samples = original;
decode_to_acescg(&mut samples, SourcePrimaries::Bt2020, SourceTransfer::SdrGamma);
decode_to_acescg(
&mut samples,
SourcePrimaries::Bt2020,
SourceTransfer::SdrGamma,
);
acescg_to_output(
&mut samples,
OutputColorSpec {
@@ -1081,7 +1097,11 @@ mod tests {
let mut samples = [1.0f32, 1.0, 1.0, 1.0];
decode_to_acescg(&mut samples, SourcePrimaries::Bt709, SourceTransfer::Linear);
for i in 0..3 {
assert!(approx(samples[i], 1.0, 1e-3), "white channel {i} = {}", samples[i]);
assert!(
approx(samples[i], 1.0, 1e-3),
"white channel {i} = {}",
samples[i]
);
}
}
@@ -1093,7 +1113,11 @@ mod tests {
assert!(approx(srgb_eotf(0.0), 0.0, 1e-6), "eotf(0)");
assert!(approx(srgb_eotf(1.0), 1.0, 1e-6), "eotf(1)");
// Mid-grey: linear 0.18 → ~0.461 sRGB code (the classic check).
assert!(approx(srgb_oetf(0.18), 0.4613, 1e-3), "oetf(0.18) = {}", srgb_oetf(0.18));
assert!(
approx(srgb_oetf(0.18), 0.4613, 1e-3),
"oetf(0.18) = {}",
srgb_oetf(0.18)
);
// Round trip.
for v in [0.0f32, 0.01, 0.18, 0.5, 0.99, 1.0] {
assert!(approx(srgb_eotf(srgb_oetf(v)), v, 1e-4), "round trip {v}");
@@ -1103,9 +1127,17 @@ mod tests {
#[test]
fn pq_transfer_anchors() {
// PQ code 1.0 = 10 000 nits = normalized 1.0.
assert!(approx(pq_eotf(1.0), 1.0, 1e-4), "pq eotf(1) = {}", pq_eotf(1.0));
assert!(
approx(pq_eotf(1.0), 1.0, 1e-4),
"pq eotf(1) = {}",
pq_eotf(1.0)
);
// PQ code 0.5 ≈ 100 nits (normalized 0.01).
assert!(approx(pq_eotf(0.5), 0.01008, 2e-3), "pq eotf(0.5) = {}", pq_eotf(0.5));
assert!(
approx(pq_eotf(0.5), 0.01008, 2e-3),
"pq eotf(0.5) = {}",
pq_eotf(0.5)
);
// Round trip.
for v in [0.0f32, 0.25, 0.5, 0.75, 1.0] {
assert!(approx(pq_eotf(pq_oetf(v)), v, 1e-3), "pq round trip {v}");
@@ -1115,9 +1147,17 @@ mod tests {
#[test]
fn hlg_transfer_anchors() {
assert!(approx(hlg_eotf(0.0), 0.0, 1e-6), "hlg eotf(0)");
assert!(approx(hlg_eotf(1.0), 1.0, 1e-3), "hlg eotf(1) = {}", hlg_eotf(1.0));
assert!(
approx(hlg_eotf(1.0), 1.0, 1e-3),
"hlg eotf(1) = {}",
hlg_eotf(1.0)
);
// The piecewise join: code 0.5 ↔ linear 1/12.
assert!(approx(hlg_eotf(0.5), 1.0 / 12.0, 1e-3), "hlg eotf(0.5) = {}", hlg_eotf(0.5));
assert!(
approx(hlg_eotf(0.5), 1.0 / 12.0, 1e-3),
"hlg eotf(0.5) = {}",
hlg_eotf(0.5)
);
// Round trip.
for v in [0.0f32, 0.1, 0.5, 0.9, 1.0] {
assert!(approx(hlg_eotf(hlg_oetf(v)), v, 1e-3), "hlg round trip {v}");
@@ -1146,18 +1186,30 @@ mod tests {
#[test]
fn setting_round_trips() {
assert_eq!(WorkingColorSpace::from_setting("acescg"), WorkingColorSpace::AcesCg);
assert_eq!(
WorkingColorSpace::from_setting("acescg"),
WorkingColorSpace::AcesCg
);
assert_eq!(
WorkingColorSpace::from_setting("srgb_legacy"),
WorkingColorSpace::SrgbLegacy
);
assert_eq!(WorkingColorSpace::from_setting("bogus"), WorkingColorSpace::AcesCg);
assert_eq!(OutputGamut::from_setting("displayp3"), OutputGamut::DisplayP3);
assert_eq!(
WorkingColorSpace::from_setting("bogus"),
WorkingColorSpace::AcesCg
);
assert_eq!(
OutputGamut::from_setting("displayp3"),
OutputGamut::DisplayP3
);
assert_eq!(OutputGamut::from_setting("bt2020"), OutputGamut::Bt2020);
assert_eq!(OutputGamut::from_setting(""), OutputGamut::Srgb);
assert_eq!(OutputTransfer::from_setting("pq"), OutputTransfer::Pq);
assert_eq!(OutputTransfer::from_setting("hlg"), OutputTransfer::Hlg);
assert_eq!(OutputTransfer::from_setting("gamma22"), OutputTransfer::Gamma22);
assert_eq!(
OutputTransfer::from_setting("gamma22"),
OutputTransfer::Gamma22
);
assert_eq!(OutputTransfer::from_setting(""), OutputTransfer::Srgb);
assert_eq!(
OutputColorSpec::from_settings("displayp3", "pq"),
@@ -1184,13 +1236,31 @@ mod tests {
fn gamma_transfer_anchors() {
// Pure-power EOTFs at code 0.5. The 2.4 case is the BT.1886
// mid-grey link: 0.5²·⁴ ≈ 0.189 (≈ the classic 18 % grey).
assert!(approx(gamma_eotf(0.5, 2.4), 0.1894646, 1e-5), "eotf(0.5, 2.4) = {}", gamma_eotf(0.5, 2.4));
assert!(approx(gamma_eotf(0.5, 2.2), 0.2176376, 1e-5), "eotf(0.5, 2.2) = {}", gamma_eotf(0.5, 2.2));
assert!(approx(gamma_eotf(0.5, 2.8), 0.1435894, 1e-5), "eotf(0.5, 2.8) = {}", gamma_eotf(0.5, 2.8));
assert!(
approx(gamma_eotf(0.5, 2.4), 0.1894646, 1e-5),
"eotf(0.5, 2.4) = {}",
gamma_eotf(0.5, 2.4)
);
assert!(
approx(gamma_eotf(0.5, 2.2), 0.2176376, 1e-5),
"eotf(0.5, 2.2) = {}",
gamma_eotf(0.5, 2.2)
);
assert!(
approx(gamma_eotf(0.5, 2.8), 0.1435894, 1e-5),
"eotf(0.5, 2.8) = {}",
gamma_eotf(0.5, 2.8)
);
// Endpoints and round trips.
for gamma in [2.2f32, 2.4, 2.8] {
assert!(approx(gamma_eotf(0.0, gamma), 0.0, 1e-6), "eotf(0) g{gamma}");
assert!(approx(gamma_eotf(1.0, gamma), 1.0, 1e-6), "eotf(1) g{gamma}");
assert!(
approx(gamma_eotf(0.0, gamma), 0.0, 1e-6),
"eotf(0) g{gamma}"
);
assert!(
approx(gamma_eotf(1.0, gamma), 1.0, 1e-6),
"eotf(1) g{gamma}"
);
for v in [0.0f32, 0.18, 0.5, 0.9, 1.0] {
assert!(
approx(gamma_eotf(gamma_oetf(v, gamma), gamma), v, 1e-4),
@@ -1244,14 +1314,20 @@ mod tests {
.map(|i: usize| ((i.wrapping_mul(2654435761)) % 1000) as f32 / 999.0)
.collect();
let reference: Vec<f32> = samples
.chunks_exact(4)
.as_chunks::<4>()
.0
.iter()
.flat_map(|px| {
let mut v = [srgb_eotf(px[0]), srgb_eotf(px[1]), srgb_eotf(px[2]), px[3]];
apply_mat(rgb_to_rgb_matrix(PRIMARIES_SRGB, PRIMARIES_AP1), &mut v);
v
})
.collect();
decode_to_acescg(&mut samples, SourcePrimaries::Bt709, SourceTransfer::SdrGamma);
decode_to_acescg(
&mut samples,
SourcePrimaries::Bt709,
SourceTransfer::SdrGamma,
);
for (i, (&got, &want)) in samples.iter().zip(reference.iter()).enumerate() {
assert!(approx(got, want, 1e-4), "pixel {i}: {got} vs {want}");
}
@@ -1267,7 +1343,11 @@ mod tests {
// (the ~1.1 system gamma belongs at final display only).
for code in [0.05f32, 0.18, 0.5, 0.75, 1.0] {
let mut samples = [code, code, code, 1.0];
decode_to_acescg(&mut samples, SourcePrimaries::Bt709, SourceTransfer::SdrGamma);
decode_to_acescg(
&mut samples,
SourcePrimaries::Bt709,
SourceTransfer::SdrGamma,
);
acescg_to_output(&mut samples, OutputColorSpec::default());
for i in 0..3 {
assert!(
@@ -1283,7 +1363,12 @@ mod tests {
bytes[0..4].copy_from_slice(&0.5f32.to_le_bytes());
bytes[4..8].copy_from_slice(&0.5f32.to_le_bytes());
bytes[8..12].copy_from_slice(&0.5f32.to_le_bytes());
decode_to_acescg_bytes(&mut bytes, 1, SourcePrimaries::Bt709, SourceTransfer::SdrGamma);
decode_to_acescg_bytes(
&mut bytes,
1,
SourcePrimaries::Bt709,
SourceTransfer::SdrGamma,
);
let r = f32::from_le_bytes(bytes[0..4].try_into().unwrap());
let m = rgb_to_rgb_matrix(PRIMARIES_SRGB, PRIMARIES_AP1);
let expected = mat_vec(m, [srgb_eotf(0.5); 3]);
@@ -1321,7 +1406,11 @@ mod tests {
samples[c],
expected[c]
);
assert!(samples[c] >= 0.0 && samples[c] <= 1.0, "channel {c} escaped [0, 1]: {}", samples[c]);
assert!(
samples[c] >= 0.0 && samples[c] <= 1.0,
"channel {c} escaped [0, 1]: {}",
samples[c]
);
}
assert_eq!(samples[3], 0.5, "alpha must be untouched");
}
@@ -1373,10 +1462,26 @@ mod tests {
let u = yuv_plane16(&[&[128 << 8], &[128 << 8]], width, stride);
let v = u.clone();
let mut out = [0.0f32; 8];
yuv444p16_to_rgb_f32(&y, stride, &u, stride, &v, stride, width, height, YuvMatrix::Bt709, false, &mut out);
yuv444p16_to_rgb_f32(
&y,
stride,
&u,
stride,
&v,
stride,
width,
height,
YuvMatrix::Bt709,
false,
&mut out,
);
for c in 0..3 {
assert!(approx(out[c], 1.0, 1e-4), "white channel {c}: {}", out[c]);
assert!(approx(out[4 + c], 0.0, 1e-4), "black channel {c}: {}", out[4 + c]);
assert!(
approx(out[4 + c], 0.0, 1e-4),
"black channel {c}: {}",
out[4 + c]
);
}
assert_eq!(out[3], 1.0, "alpha");
assert_eq!(out[7], 1.0, "alpha");
@@ -1394,7 +1499,19 @@ mod tests {
let u = yuv_plane16(&[&[11059]], width, stride);
let v = yuv_plane16(&[&[5329]], width, stride);
let mut out = [0.0f32; 4];
yuv444p16_to_rgb_f32(&y, stride, &u, stride, &v, stride, width, height, YuvMatrix::Bt601, true, &mut out);
yuv444p16_to_rgb_f32(
&y,
stride,
&u,
stride,
&v,
stride,
width,
height,
YuvMatrix::Bt601,
true,
&mut out,
);
assert!(approx(out[0], 0.0, 1e-3), "R = {}", out[0]);
assert!(approx(out[1], 1.0, 1e-3), "G = {}", out[1]);
assert!(approx(out[2], 0.0, 1e-3), "B = {}", out[2]);
@@ -1412,7 +1529,19 @@ mod tests {
let u = yuv_plane16(&[&[10666]], width, stride);
let v = yuv_plane16(&[&[6725]], width, stride);
let mut out = [0.0f32; 4];
yuv444p16_to_rgb_f32(&y, stride, &u, stride, &v, stride, width, height, YuvMatrix::Bt709, false, &mut out);
yuv444p16_to_rgb_f32(
&y,
stride,
&u,
stride,
&v,
stride,
width,
height,
YuvMatrix::Bt709,
false,
&mut out,
);
assert!(approx(out[0], 0.0, 1e-3), "R = {}", out[0]);
assert!(approx(out[1], 1.0, 1e-3), "G = {}", out[1]);
assert!(approx(out[2], 0.0, 1e-3), "B = {}", out[2]);
@@ -1423,11 +1552,35 @@ mod tests {
// A 2×2 frame at stride 4 needs (2−1)·4 + 2·2 = 8 bytes per plane;
// 4 bytes is too short → early return, `out` untouched.
let mut out = [7.0f32; 4];
yuv444p16_to_rgb_f32(&[0u8; 4], 4, &[0u8; 4], 4, &[0u8; 4], 4, 2, 2, YuvMatrix::Bt709, false, &mut out);
yuv444p16_to_rgb_f32(
&[0u8; 4],
4,
&[0u8; 4],
4,
&[0u8; 4],
4,
2,
2,
YuvMatrix::Bt709,
false,
&mut out,
);
assert_eq!(out, [7.0; 4]);
// `out` too short for the frame → early return.
let mut out2 = [7.0f32; 3];
yuv444p16_to_rgb_f32(&[0u8; 8], 4, &[0u8; 8], 4, &[0u8; 8], 4, 2, 2, YuvMatrix::Bt709, false, &mut out2);
yuv444p16_to_rgb_f32(
&[0u8; 8],
4,
&[0u8; 8],
4,
&[0u8; 8],
4,
2,
2,
YuvMatrix::Bt709,
false,
&mut out2,
);
assert_eq!(out2, [7.0; 3]);
}
+43 -14
View File
@@ -308,10 +308,7 @@ impl ConfigStore {
Some(slash) => {
let group = key[..slash].to_string();
let sub = key[slash + 1..].to_string();
sections
.entry(group)
.or_default()
.insert(sub, toml_value);
sections.entry(group).or_default().insert(sub, toml_value);
}
None => {
sections
@@ -823,7 +820,7 @@ fn format_g(v: f64) -> String {
let sci = format!("{:.*e}", (PRECISION - 1) as usize, a); // "d.ddddd e±N"
let exp = sci.split('e').nth(1).unwrap().parse::<i32>().unwrap();
let xr = exp;
let body = if xr < -4 || xr >= PRECISION {
let body = if !(-4..PRECISION).contains(&xr) {
// Scientific notation: mantissa is already rounded; strip trailing zeros.
let mant = sci.split('e').next().unwrap();
let m = trim_mantissa(mant);
@@ -1164,7 +1161,11 @@ mod tests {
let content = std::fs::read_to_string(dir.join("config.toml")).unwrap();
// Native TOML types: ints/bools/floats unquoted, strings quoted.
assert!(content.contains("FlatKey = 1"), "content: {}", content);
assert!(content.contains("StrKey = \"hello\""), "content: {}", content);
assert!(
content.contains("StrKey = \"hello\""),
"content: {}",
content
);
assert!(content.contains("DblKey = 2.5"), "content: {}", content);
assert!(content.contains("BoolKey = true"), "content: {}", content);
// Grouped keys land in [group] tables.
@@ -1262,9 +1263,21 @@ UnknownTypedThing=hello
assert_eq!(s.get_double(Some("render"), "gain", -1.0), -1.0);
// The file holds native TOML types, not stringified values.
let content = std::fs::read_to_string(dir.join("config.toml")).unwrap();
assert!(content.contains("DefaultSequenceWidth = 640"), "content: {}", content);
assert!(content.contains("SplitClipsCopyNodes = false"), "content: {}", content);
assert!(content.contains("ProxyPreset = \"medium\""), "content: {}", content);
assert!(
content.contains("DefaultSequenceWidth = 640"),
"content: {}",
content
);
assert!(
content.contains("SplitClipsCopyNodes = false"),
"content: {}",
content
);
assert!(
content.contains("ProxyPreset = \"medium\""),
"content: {}",
content
);
assert!(content.contains("gain = 1.5"), "content: {}", content);
});
}
@@ -1293,10 +1306,22 @@ CustomKey=hello
assert!(dir.join("config.toml").is_file());
assert!(dir.join("config.ini").is_file());
let content = std::fs::read_to_string(dir.join("config.toml")).unwrap();
assert!(content.contains("DefaultSequenceWidth = 640"), "content: {}", content);
assert!(content.contains("UseProxyMedia = false"), "content: {}", content);
assert!(
content.contains("DefaultSequenceWidth = 640"),
"content: {}",
content
);
assert!(
content.contains("UseProxyMedia = false"),
"content: {}",
content
);
assert!(content.contains("[section]"), "content: {}", content);
assert!(content.contains("CustomKey = \"hello\""), "content: {}", content);
assert!(
content.contains("CustomKey = \"hello\""),
"content: {}",
content
);
// A second load reads the migrated TOML, not the INI.
s.reset_defaults().unwrap();
@@ -1615,7 +1640,11 @@ CustomKey=hello
// No group defaults exist; every top-level key is a plain value.
assert!(table.values().all(|v| !v.is_table()));
// Serialized output must not contain INI-style "k=v" lines.
assert!(!content.contains("DefaultSequenceWidth=1920"), "content: {}", content);
assert!(
!content.contains("DefaultSequenceWidth=1920"),
"content: {}",
content
);
});
}
@@ -1698,7 +1727,7 @@ FlatAfterEmptySection=ok
assert_eq!(s.get(Some("g"), "KeyWithEquals").unwrap(), "a=b");
// "[]" empties the group, so the key is flat.
assert_eq!(s.get(None, "FlatAfterEmptySection").unwrap(), "ok");
assert!(matches!(s.get(Some(""), "FlatAfterEmptySection"), Ok(_)));
assert!(s.get(Some(""), "FlatAfterEmptySection").is_ok());
});
}
+18 -10
View File
@@ -229,9 +229,7 @@ fn parse_xprop_icc_hex(output: &str) -> Option<Vec<u8>> {
n_digits += 1;
}
if n_digits > 0 {
if let Ok(byte) =
u8::from_str_radix(&rest[digits_start..digits_start + n_digits], 16)
{
if let Ok(byte) = u8::from_str_radix(&rest[digits_start..digits_start + n_digits], 16) {
bytes.push(byte);
}
}
@@ -563,9 +561,8 @@ mod windows {
let sub_key = wide_nul(r"Software\Microsoft\Windows\CurrentVersion\VideoSettings");
let value_name = wide_nul("EnableAutoColorManagement");
let mut key: *mut c_void = std::ptr::null_mut();
let status = unsafe {
RegOpenKeyExW(HKEY_CURRENT_USER, sub_key.as_ptr(), 0, KEY_READ, &mut key)
};
let status =
unsafe { RegOpenKeyExW(HKEY_CURRENT_USER, sub_key.as_ptr(), 0, KEY_READ, &mut key) };
if status != ERROR_SUCCESS {
return None;
}
@@ -628,7 +625,12 @@ mod windows {
pub(super) fn display_icc_for(device: &str) -> Option<String> {
let name = wide_nul(device);
let hdc = unsafe {
CreateDCW(name.as_ptr(), name.as_ptr(), std::ptr::null(), std::ptr::null())
CreateDCW(
name.as_ptr(),
name.as_ptr(),
std::ptr::null(),
std::ptr::null(),
)
};
if hdc.is_null() {
return None;
@@ -660,7 +662,10 @@ mod windows {
sz_device: [0; 32],
};
let ok = unsafe {
GetMonitorInfoW(hmonitor as *const c_void, &mut info as *mut _ as *mut c_void)
GetMonitorInfoW(
hmonitor as *const c_void,
&mut info as *mut _ as *mut c_void,
)
};
if ok == 0 {
return None;
@@ -864,7 +869,7 @@ mod linux {
.lines()
.map(str::trim_start)
.find(|l| l.starts_with("Filename:"))?;
let name = line.splitn(2, ':').nth(1)?.trim();
let name = line.split_once(':')?.1.trim();
if name.is_empty() {
None
} else {
@@ -1066,7 +1071,10 @@ Monitors: 1
#[test]
fn parse_xrandr_monitors_malformed() {
assert_eq!(parse_xrandr_monitors(""), Vec::<RandRMonitor>::new());
assert_eq!(parse_xrandr_monitors("Monitors: 0\n"), Vec::<RandRMonitor>::new());
assert_eq!(
parse_xrandr_monitors("Monitors: 0\n"),
Vec::<RandRMonitor>::new()
);
// No `:` separator, a missing geometry, and a geometry with no
// outputs are all skipped without panicking.
let out = "\
+28 -28
View File
@@ -51,7 +51,7 @@ fn ends_with_case_insensitive(s: &str, suffix: &str) -> bool {
s.as_bytes()[offset..]
.iter()
.zip(suffix.as_bytes().iter())
.all(|(a, b)| a.to_ascii_lowercase() == b.to_ascii_lowercase())
.all(|(a, b)| a.eq_ignore_ascii_case(b))
}
/// The filefunctions family (stateless; the handle only exists for C ABI
@@ -424,6 +424,33 @@ pub(crate) fn config_location_path() -> Result<PathBuf> {
.map(PathBuf::from)
}
/// The default disk cache directory (C++ `DiskManager::
/// get_default_disk_cache_path`): `<configuration location>/mediacache`.
///
/// The `DiskCachePath` config key overrides the location when set (the
/// preferences dialog's cache-directory setting); an empty/absent value
/// keeps the default.
///
/// Single-lib unification: this used to live in the oakrender crate's
/// `bridge::common` fallback (see `docs/zh/plans/riir/single-lib.md`);
/// oaknode and oakrender both call it directly now.
pub fn default_disk_cache_path() -> String {
// A configured override wins (whitespace-only counts as absent).
if let Ok(custom) = crate::configstore::ConfigStore::instance().get(None, "DiskCachePath") {
if !custom.trim().is_empty() {
return custom;
}
}
Path::new(
&FileFunctions::new()
.get_configuration_location()
.unwrap_or_default(),
)
.join("mediacache")
.to_string_lossy()
.into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -927,30 +954,3 @@ mod tests {
let _ = std::fs::remove_dir_all(&b);
}
}
/// The default disk cache directory (C++ `DiskManager::
/// get_default_disk_cache_path`): `<configuration location>/mediacache`.
///
/// The `DiskCachePath` config key overrides the location when set (the
/// preferences dialog's cache-directory setting); an empty/absent value
/// keeps the default.
///
/// Single-lib unification: this used to live in the oakrender crate's
/// `bridge::common` fallback (see `docs/zh/plans/riir/single-lib.md`);
/// oaknode and oakrender both call it directly now.
pub fn default_disk_cache_path() -> String {
// A configured override wins (whitespace-only counts as absent).
if let Ok(custom) = crate::configstore::ConfigStore::instance().get(None, "DiskCachePath") {
if !custom.trim().is_empty() {
return custom;
}
}
Path::new(
&FileFunctions::new()
.get_configuration_location()
.unwrap_or_default(),
)
.join("mediacache")
.to_string_lossy()
.into_owned()
}
+1 -1
View File
@@ -482,7 +482,7 @@ mod tests {
assert!(cur.get_video_params().unwrap().is_null());
// Store with a destructor.
let p1 = 0x1 as *mut c_void;
let p1 = std::ptr::dangling_mut::<c_void>();
assert!(cur.set_video_params(p1, Some(count_destroy)).is_ok());
assert_eq!(cur.get_video_params().unwrap(), p1);
+2 -2
View File
@@ -271,10 +271,10 @@ impl SubtitleParams {
out.push_str("<subtitle");
out.push_str(" in=\"");
out.push_str(&escape_attribute(&rational_to_string(sub.in_time)));
out.push_str("\"");
out.push('"');
out.push_str(" out=\"");
out.push_str(&escape_attribute(&rational_to_string(sub.out_time)));
out.push_str("\"");
out.push('"');
out.push('>');
out.push_str(&escape_text(&sub.text));
out.push_str("</subtitle>");
+2 -2
View File
@@ -16,8 +16,8 @@
//! Textures and CPU frames.
use crate::Rational;
use crate::PixelFormat;
use crate::Rational;
use std::sync::Arc;
use crate::backend::{BackendKind, GpuContextLike};
@@ -385,7 +385,7 @@ mod tests {
let pod = f.video_params();
assert_eq!(pod.width, 3);
assert_eq!(pod.pixel_aspect_num, 2);
assert_eq!(f.is_float(), true);
assert!(f.is_float());
assert_eq!(f.pixel_count(), 6);
f.allocate();
assert!(!f.data().is_null());
+4 -4
View File
@@ -703,9 +703,9 @@ impl VideoParams {
let divider = squared_divider.sqrt();
if divider <= DIVIDERS[0] as f64 {
return DIVIDERS[0];
DIVIDERS[0]
} else if divider >= DIVIDERS[7] as f64 {
return DIVIDERS[7];
DIVIDERS[7]
} else {
for i in 1..DIVIDERS.len() {
let prev = DIVIDERS[i - 1];
@@ -716,7 +716,7 @@ impl VideoParams {
return if prev_diff < next_diff { prev } else { next };
}
}
return 1; // fallback, unreachable
1 // fallback, unreachable
}
}
@@ -1075,7 +1075,7 @@ fn format_g(value: f64) -> String {
e += 1;
}
let body = if PREC > e && e >= -4 {
let body = if (-4..PREC).contains(&e) {
let decimals = (PREC - 1 - e).max(0) as usize;
trim_frac_trailing(format!("{:.*}", decimals, v))
} else {