Files
oak-editor/src/oakui/frames.rs
T
Mike-Solar 7b295b7661 feat(app): scopes (histogram/waveform/vectorscope) fed by rendered frames
- program viewer gains Picture/Scopes tabs; the scopes page hosts the
  gpui_widgets histogram, waveform and vectorscope side by side
- RealEngine analyzes the same F32 RGBA samples it renders (BT.709 luma,
  normalized Cb/Cr); the mock engine analyzes its synthetic frame
  through the same path; results ride the per-frame cache
- AppEngine::scope_data(monitor) exposes ScopeData to panels
- unit tests for the analysis math + a gpui test rendering the scopes
  tab; zh/en i18n keys added
2026-08-11 17:04:42 +08:00

130 lines
4.9 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/>.
//! The synthetic CPU viewer frame both engines display.
//!
//! The real engine delivers frames through the render worker (a separate
//! process speaking the NDJSON control-plane protocol, `oakengine::worker`),
//! which is out of scope for this increment. Until that transport is wired,
//! both the mock and the real engine feed the viewers the same SMPTE-style
//! test pattern, so playback is visibly moving while the engine metadata
//! (project / sequence / tracks) comes from the real facade in real mode.
use gpui::timeline::Frame;
use gpui::RenderImage;
/// Width of the synthetic test frame (a small proxy size; the real engine
/// will deliver full-resolution frames).
pub(crate) const SYNTH_FRAME_WIDTH: u32 = 384;
/// Height of the synthetic test frame.
pub(crate) const SYNTH_FRAME_HEIGHT: u32 = 216;
/// Generates the F32 RGBA samples of the synthetic test frame: SMPTE-style
/// color bars with a white sweep whose x position follows `frame`, so
/// transport playback shows up as motion across the picture.
///
/// The samples mirror the real engine's pixel format; callers downconvert
/// them to BGRA8 for the viewer's CPU-frame path and analyze the scope
/// samples from the very same buffer, so the scopes read exactly what the
/// viewer displays.
pub(crate) fn synthetic_frame_samples(frame: Frame) -> (u32, u32, Vec<f32>) {
let width = SYNTH_FRAME_WIDTH;
let height = SYNTH_FRAME_HEIGHT;
// F32 RGBA samples; the caller downconverts to BGRA8 for the sprite atlas.
let mut samples = vec![0.0f32; (width * height * 4) as usize];
// SMPTE bars: 75% white, yellow, cyan, green, magenta, red, blue.
let bars: [(f32, f32, f32); 7] = [
(1.0, 1.0, 1.0),
(1.0, 1.0, 0.0),
(0.0, 1.0, 1.0),
(0.0, 1.0, 0.0),
(1.0, 0.0, 1.0),
(1.0, 0.0, 0.0),
(0.0, 0.0, 1.0),
];
// Bottom strip: blue, magenta, 75% white, black.
let strip: [(f32, f32, f32); 4] = [
(0.0, 0.0, 1.0),
(1.0, 0.0, 1.0),
(0.75, 0.75, 0.75),
(0.0, 0.0, 0.0),
];
// The sweep moves 6 px per frame and wraps around the width, so
// transport playback shows up as motion across the picture.
let sweep = (frame.0 as f32 * 6.0) % width as f32;
let bars_top = height as f32 * 0.66;
for y in 0..height {
for x in 0..width {
let in_sweep = (x as f32 - sweep).abs() < 6.0;
let color = if in_sweep {
(1.0, 1.0, 1.0)
} else if (y as f32) < bars_top {
bars[((x as f32 / width as f32) * 7.0) as usize]
} else {
strip[((x as f32 / width as f32) * 4.0) as usize]
};
let i = ((y * width + x) * 4) as usize;
samples[i] = color.0;
samples[i + 1] = color.1;
samples[i + 2] = color.2;
samples[i + 3] = 1.0;
}
}
(width, height, samples)
}
/// Downconverts an F32 RGBA frame (the engine pipeline's pixel format) to a
/// BGRA8 [`RenderImage`] for the viewers' CPU-frame path. Samples are
/// clamped to `0.0..=1.0` before quantization; `samples` must hold exactly
/// `width * height * 4` values (tightly packed rows).
///
/// Shared by the synthetic test pattern and the real engine's rendered
/// frames ([`super::real`]).
pub(crate) fn f32_rgba_to_bgra_image(width: u32, height: u32, samples: &[f32]) -> RenderImage {
assert_eq!(
samples.len(),
(width * height * 4) as usize,
"F32 RGBA frame must be tightly packed"
);
let mut bytes = Vec::with_capacity(samples.len());
for i in (0..samples.len()).step_by(4) {
bytes.push((samples[i + 2].clamp(0.0, 1.0) * 255.0) as u8); // B
bytes.push((samples[i + 1].clamp(0.0, 1.0) * 255.0) as u8); // G
bytes.push((samples[i].clamp(0.0, 1.0) * 255.0) as u8); // R
bytes.push((samples[i + 3].clamp(0.0, 1.0) * 255.0) as u8); // A
}
let buffer = image::RgbaImage::from_raw(width, height, bytes).expect("BGRA frame");
RenderImage::new(smallvec::SmallVec::from_elem(image::Frame::new(buffer), 1))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn f32_rgba_converts_to_bgra_bytes() {
// One red-ish pixel and one pixel exercising clamping.
let samples = [1.0, 0.0, 0.5, 1.0, 2.0, -1.0, 0.25, 1.0];
let image = f32_rgba_to_bgra_image(2, 1, &samples);
let frame = image.as_bytes(0).expect("one frame"); // BGRA8, tightly packed
assert_eq!(&frame[0..4], &[127, 0, 255, 255]);
assert_eq!(&frame[4..8], &[63, 0, 255, 255]);
}
}