Preview now follows the project output colorspace end to end: the display chain derives its content space from the project's OutputColorSpec instead of a hardcoded sRGB name, self-managed ICC transforms go through an XYZ D65 interchange stage (OCIO cie_xyz_d65_interchange) for non-sRGB targets, and the platform layer declares the content colorspace (gpui submodule bump). macOS defaults to OS-managed (fixes wide-gamut UI oversaturation); Windows ACM warns once on non-sRGB targets. Multi-monitor: the display ICC is looked up per the window's current screen (macOS display id, Windows per-monitor DC, X11 RandR output profile) with a throttled poll that invalidates frame caches on moves. Pipeline precision: 10-bit+ sources fall back to YUV444P16LE + a Rust matrix conversion when swscale lacks F32 output (no more 8-bit truncation); BT.709/2020 SDR decodes with BT.1886 gamma 2.4 instead of the sRGB EOTF; working-space compositing no longer clamps RGB to [0,1] (alpha still clamped); the output node clamps to the target gamut; frames without colorimetry metadata convert with BT.709 defaults (warned once) instead of passing through; scopes read the output-colorspace signal on both F32 paths. Also: only emit rerun-if-changed for .env when it exists (a missing file made every build fully dirty).
192 lines
5.4 KiB
Rust
192 lines
5.4 KiB
Rust
// Oak Video Editor - Non-Linear Video Editor
|
|
// Copyright (C) 2026 Oak Team
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
//! A minimal stderr backend for the `log` facade.
|
|
//!
|
|
//! The gpui / wgpu / oak stack all emit through `log::*`, but nothing in the
|
|
//! binary installed a logger, so every validation error, adapter warning and
|
|
//! diagnostic was silently dropped (this is what made the viewer-black-screen
|
|
//! fault invisible for so long). [`init`] installs a small logger that writes
|
|
//! to stderr; `RUST_LOG` selects the verbosity:
|
|
//!
|
|
//! * unset — `warn` and above;
|
|
//! * a level name (`error` / `warn` / `info` / `debug` / `trace`) — that
|
|
//! level and above for every target;
|
|
//! * comma-separated `target=level` pairs (`wgpu=debug,oak_render=info`) —
|
|
//! per-target overrides on top of the global level. A bare level among the
|
|
//! pairs sets the global level.
|
|
|
|
use std::sync::Once;
|
|
|
|
use log::{Level, LevelFilter, Metadata, Record};
|
|
|
|
/// One parsed `RUST_LOG` directive set.
|
|
struct Filter {
|
|
/// The global (default) level.
|
|
global: LevelFilter,
|
|
/// Per-target overrides (`target` prefix → level).
|
|
targets: Vec<(String, LevelFilter)>,
|
|
}
|
|
|
|
impl Filter {
|
|
/// Parse the `RUST_LOG` value (`None` / empty → warn-only).
|
|
fn parse(spec: Option<&str>) -> Self {
|
|
let mut global = LevelFilter::Warn;
|
|
let mut targets = Vec::new();
|
|
if let Some(spec) = spec.filter(|s| !s.trim().is_empty()) {
|
|
for part in spec.split(',') {
|
|
let part = part.trim();
|
|
if part.is_empty() {
|
|
continue;
|
|
}
|
|
match part.split_once('=') {
|
|
Some((target, level)) => {
|
|
if let Some(level) = parse_level(level.trim()) {
|
|
targets.push((target.trim().to_string(), level));
|
|
}
|
|
}
|
|
None => {
|
|
// A bare level sets the global filter.
|
|
if let Some(level) = parse_level(part) {
|
|
global = level;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Self { global, targets }
|
|
}
|
|
|
|
/// The effective level for `target` (longest matching prefix wins).
|
|
fn level_for(&self, target: &str) -> LevelFilter {
|
|
let mut best: Option<(usize, LevelFilter)> = None;
|
|
for (prefix, level) in &self.targets {
|
|
if target.starts_with(prefix.as_str()) {
|
|
let len = prefix.len();
|
|
if best.map(|(b, _)| len >= b).unwrap_or(true) {
|
|
best = Some((len, *level));
|
|
}
|
|
}
|
|
}
|
|
best.map(|(_, level)| level).unwrap_or(self.global)
|
|
}
|
|
}
|
|
|
|
/// Map a level name to a [`LevelFilter`].
|
|
fn parse_level(text: &str) -> Option<LevelFilter> {
|
|
match text.to_ascii_lowercase().as_str() {
|
|
"off" => Some(LevelFilter::Off),
|
|
"error" => Some(LevelFilter::Error),
|
|
"warn" | "warning" => Some(LevelFilter::Warn),
|
|
"info" => Some(LevelFilter::Info),
|
|
"debug" => Some(LevelFilter::Debug),
|
|
"trace" => Some(LevelFilter::Trace),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// The logger installed by [`init`].
|
|
struct StderrLogger {
|
|
filter: Filter,
|
|
}
|
|
|
|
impl log::Log for StderrLogger {
|
|
fn enabled(&self, metadata: &Metadata) -> bool {
|
|
metadata.level() <= self.filter.level_for(metadata.target())
|
|
}
|
|
|
|
fn log(&self, record: &Record) {
|
|
if !self.enabled(record.metadata()) {
|
|
return;
|
|
}
|
|
let level = match record.level() {
|
|
Level::Error => "ERROR",
|
|
Level::Warn => "WARN",
|
|
Level::Info => "INFO",
|
|
Level::Debug => "DEBUG",
|
|
Level::Trace => "TRACE",
|
|
};
|
|
eprintln!(
|
|
"[{level} {}] {}",
|
|
record.target(),
|
|
record.args()
|
|
);
|
|
}
|
|
|
|
fn flush(&self) {}
|
|
}
|
|
|
|
static INIT: Once = Once::new();
|
|
|
|
/// Install the stderr logger once (idempotent). Honors `RUST_LOG`.
|
|
pub fn init() {
|
|
INIT.call_once(|| {
|
|
let filter = Filter::parse(std::env::var("RUST_LOG").ok().as_deref());
|
|
let max = filter
|
|
.targets
|
|
.iter()
|
|
.map(|(_, level)| *level)
|
|
.max()
|
|
.unwrap_or(LevelFilter::Off)
|
|
.max(filter.global);
|
|
let logger = Box::leak(Box::new(StderrLogger { filter }));
|
|
if log::set_logger(logger).is_ok() {
|
|
log::set_max_level(max);
|
|
}
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn default_is_warn() {
|
|
let f = Filter::parse(None);
|
|
assert_eq!(f.level_for("anything"), LevelFilter::Warn);
|
|
assert_eq!(f.level_for(""), LevelFilter::Warn);
|
|
}
|
|
|
|
#[test]
|
|
fn bare_level_sets_global() {
|
|
let f = Filter::parse(Some("info"));
|
|
assert_eq!(f.level_for("wgpu_hal"), LevelFilter::Info);
|
|
assert_eq!(f.level_for("oak_render"), LevelFilter::Info);
|
|
}
|
|
|
|
#[test]
|
|
fn target_overrides_with_longest_prefix() {
|
|
let f = Filter::parse(Some("warn,wgpu=debug,wgpu_hal=trace"));
|
|
assert_eq!(f.level_for("wgpu"), LevelFilter::Debug);
|
|
assert_eq!(f.level_for("wgpu_core"), LevelFilter::Debug);
|
|
assert_eq!(f.level_for("wgpu_hal::vulkan"), LevelFilter::Trace);
|
|
assert_eq!(f.level_for("oak_render"), LevelFilter::Warn);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_level_aliases() {
|
|
assert_eq!(parse_level("warning"), Some(LevelFilter::Warn));
|
|
assert_eq!(parse_level("OFF"), Some(LevelFilter::Off));
|
|
assert_eq!(parse_level("bogus"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_spec_falls_back_to_warn() {
|
|
let f = Filter::parse(Some(" "));
|
|
assert_eq!(f.level_for("x"), LevelFilter::Warn);
|
|
}
|
|
}
|