Merge pull request #220 from storytold/fix/198-201-204-app-startup
App: starts with an empty project, opens on older GPUs, shows comps wider than the texture limit (#198, #201, #204)
This commit is contained in:
commit
0e6d37061a
|
|
@ -8,7 +8,7 @@ work apply to everyone, people and AI agents alike, and they are not negotiable.
|
|||
You need [Rust](https://rustup.rs/) 1.95 or newer.
|
||||
|
||||
```sh
|
||||
cargo run -p effectcraft # the desktop app, with the demo project
|
||||
cargo run -p effectcraft -- --demo # the desktop app, with the demo project
|
||||
cargo test --workspace
|
||||
cargo xtask ci # what every commit must pass
|
||||
```
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ To build it yourself you need [Rust](https://rustup.rs/) 1.95 or newer.
|
|||
```sh
|
||||
git clone https://github.com/storytold/effectcraft
|
||||
cd effectcraft
|
||||
cargo run --release -p effectcraft # the app, with the demo project open
|
||||
cargo run --release -p effectcraft # the app (add `-- --demo` to open the demo project)
|
||||
cargo run --release -p effectcraft-cli -- render --out intro.mp4 # render the demo headless
|
||||
cargo xtask web --serve 8765 # the browser build on http://127.0.0.1:8765/ (docs/web.md)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
//!
|
||||
//! Usage: `effectcraft [--control <port>] [--demo|--empty|--home] [project.ecproj | media files…]`
|
||||
//!
|
||||
//! Without a project the app starts with an empty Untitled Project, as After Effects does;
|
||||
//! `--demo` opens the demo project instead (also Help ▸ Open Demo Project).
|
||||
//!
|
||||
//! `--control <port>` (or `EFFECTCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server;
|
||||
//! see `effectcraft_ui_egui::control` for the methods.
|
||||
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
|
||||
|
|
@ -32,28 +35,43 @@ fn app_icon() -> egui::IconData {
|
|||
eframe::icon_data::from_png_bytes(png).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The command line.
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
struct Args {
|
||||
control_port: Option<u16>,
|
||||
files: Vec<String>,
|
||||
/// Open the demo project when no project is given (`--demo`).
|
||||
demo: bool,
|
||||
/// Show or skip the Home screen (`--home`), else the Startup preference decides.
|
||||
home: Option<bool>,
|
||||
}
|
||||
|
||||
/// Parse the arguments after the program name; `None` for `--version`. `env_port` is
|
||||
/// `EFFECTCRAFT_CONTROL_PORT`.
|
||||
fn parse_args(args: impl IntoIterator<Item = String>, env_port: Option<String>) -> Option<Args> {
|
||||
let mut out = Args { control_port: env_port.and_then(|p| p.parse().ok()), ..Default::default() };
|
||||
let mut args = args.into_iter();
|
||||
while let Some(a) = args.next() {
|
||||
match a.as_str() {
|
||||
"--control" => out.control_port = args.next().and_then(|p| p.parse().ok()),
|
||||
"--demo" => out.demo = true,
|
||||
"--empty" => out.demo = false,
|
||||
"--home" => out.home = Some(true),
|
||||
"--version" => return None,
|
||||
_ => out.files.push(a),
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
// Help ▸ Enable Logging writes through this logger; warnings feed the compatibility report.
|
||||
effectcraft_engine::logging::install();
|
||||
effectcraft_engine::logging::install_panic_hook();
|
||||
let mut control_port: Option<u16> = std::env::var("EFFECTCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok());
|
||||
let mut files = Vec::new();
|
||||
let mut demo = true;
|
||||
let mut home: Option<bool> = None;
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(a) = args.next() {
|
||||
match a.as_str() {
|
||||
"--control" => control_port = args.next().and_then(|p| p.parse().ok()),
|
||||
"--demo" => demo = true,
|
||||
"--empty" => demo = false,
|
||||
"--home" => home = Some(true),
|
||||
"--version" => {
|
||||
println!("effectcraft {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(());
|
||||
}
|
||||
_ => files.push(a),
|
||||
}
|
||||
}
|
||||
let Some(Args { control_port, files, demo, home }) = parse_args(std::env::args().skip(1), std::env::var("EFFECTCRAFT_CONTROL_PORT").ok()) else {
|
||||
println!("effectcraft {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(());
|
||||
};
|
||||
// The font menus list the installed fonts: read their names while the window opens.
|
||||
effectcraft_engine::text::fonts::scan_system_in_background();
|
||||
// Wayland shows the window's icon from its desktop entry: an AppImage brings its own.
|
||||
|
|
@ -71,9 +89,10 @@ fn main() -> eframe::Result {
|
|||
// Wayland matches the window to `ai.storyteller.effectcraft.desktop` by this ID.
|
||||
.with_app_id(appimage::APP_ID),
|
||||
event_loop_builder: agent_event_loop(control_port.is_some()),
|
||||
wgpu_options: wgpu_options(),
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
let result = eframe::run_native(
|
||||
"EffectCraft",
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
|
|
@ -170,7 +189,50 @@ fn main() -> eframe::Result {
|
|||
app,
|
||||
}))
|
||||
}),
|
||||
)
|
||||
);
|
||||
if let Err(e) = &result {
|
||||
startup_failed(e);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// The window's wgpu device asks for the adapter's own limits (textures up to 16384 px)
|
||||
/// instead of eframe's fixed set: an older GPU that can't meet those (Intel HD Graphics 5500,
|
||||
/// #198) still opens the app, and the compositor then checks what it needs and falls back to
|
||||
/// the CPU when the device can't run it.
|
||||
fn wgpu_options() -> eframe::WgpuConfiguration {
|
||||
use eframe::egui_wgpu::WgpuSetup;
|
||||
let mut config = eframe::WgpuConfiguration::default();
|
||||
if let WgpuSetup::CreateNew(setup) = &mut config.wgpu_setup {
|
||||
setup.device_descriptor = std::sync::Arc::new(|adapter| eframe::wgpu::DeviceDescriptor {
|
||||
label: Some("EffectCraft device"),
|
||||
required_limits: device_limits(adapter.limits()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
config
|
||||
}
|
||||
|
||||
/// The limits to request from an adapter that offers `adapter`: all of them, with textures
|
||||
/// capped at 16384 px like the headless renderer's device.
|
||||
fn device_limits(adapter: eframe::wgpu::Limits) -> eframe::wgpu::Limits {
|
||||
eframe::wgpu::Limits { max_texture_dimension_2d: adapter.max_texture_dimension_2d.min(16384), ..adapter }
|
||||
}
|
||||
|
||||
/// The window couldn't open (no usable graphics adapter or device): say so instead of quitting
|
||||
/// silently, since release builds on Windows have no console.
|
||||
fn startup_failed(e: &eframe::Error) {
|
||||
let message = format!(
|
||||
"EffectCraft couldn't start its window: {e}\n\nUpdating the graphics driver often helps. If it keeps happening, please report it at https://github.com/storytold/effectcraft/issues with this message."
|
||||
);
|
||||
log::error!("{message}");
|
||||
eprintln!("effectcraft: {message}");
|
||||
let _ = rfd::MessageDialog::new()
|
||||
.set_level(rfd::MessageLevel::Error)
|
||||
.set_title("EffectCraft")
|
||||
.set_description(message)
|
||||
.set_buttons(rfd::MessageButtons::Ok)
|
||||
.show();
|
||||
}
|
||||
|
||||
/// The desktop app: the egui app plus, on macOS, the system menu bar.
|
||||
|
|
@ -227,3 +289,37 @@ fn disable_app_nap() {
|
|||
std::mem::forget(info.beginActivityWithOptions_reason(opts, &reason));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Args, parse_args};
|
||||
|
||||
fn parse(args: &[&str]) -> Option<Args> {
|
||||
parse_args(args.iter().map(|a| a.to_string()), None)
|
||||
}
|
||||
|
||||
/// The window's device never asks for more than the adapter offers (#198).
|
||||
#[test]
|
||||
fn device_limits_fit_the_adapter() {
|
||||
use eframe::wgpu::Limits;
|
||||
for adapter in [Limits::downlevel_defaults(), Limits::downlevel_webgl2_defaults(), Limits::default()] {
|
||||
let l = super::device_limits(adapter.clone());
|
||||
assert!(l.check_limits(&adapter), "{l:?}");
|
||||
assert_eq!(l.max_storage_buffers_per_shader_stage, adapter.max_storage_buffers_per_shader_stage);
|
||||
}
|
||||
let big = Limits { max_texture_dimension_2d: 32768, ..Limits::default() };
|
||||
assert_eq!(super::device_limits(big).max_texture_dimension_2d, 16384);
|
||||
}
|
||||
|
||||
/// Launching without a project opens an empty project, not the demo (#204).
|
||||
#[test]
|
||||
fn no_arguments_start_an_empty_project() {
|
||||
assert_eq!(parse(&[]), Some(Args::default()));
|
||||
assert!(parse(&["--demo"]).is_some_and(|a| a.demo));
|
||||
assert!(parse(&["--demo", "--empty"]).is_some_and(|a| !a.demo));
|
||||
let a = parse(&["--control", "9877", "--home", "a.ecproj"]).unwrap_or_default();
|
||||
assert_eq!((a.control_port, a.home, a.files), (Some(9877), Some(true), vec!["a.ecproj".to_string()]));
|
||||
assert_eq!(parse(&["--version"]), None);
|
||||
assert_eq!(parse_args(Vec::new(), Some("9000".into())).map(|a| a.control_port), Some(Some(9000)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -425,6 +425,55 @@ pub fn to_color_image(img: &effectcraft_engine::render::Image) -> egui::ColorIma
|
|||
egui::ColorImage::new([img.width as usize, img.height as usize], px)
|
||||
}
|
||||
|
||||
/// The size `size` is shown at in a texture whose sides can't exceed `max_side`: divided by the
|
||||
/// smallest whole factor that fits (frames of comps wider than the GPU's texture limit, #201).
|
||||
pub fn fitted_size(size: [usize; 2], max_side: usize) -> [usize; 2] {
|
||||
let k = size[0].max(size[1]).div_ceil(max_side.max(1)).max(1);
|
||||
[size[0].div_ceil(k), size[1].div_ceil(k)]
|
||||
}
|
||||
|
||||
/// `img` as a texture the renderer accepts: unchanged when it fits in `max_side`, else averaged
|
||||
/// down by a whole factor (premultiplied pixels average correctly). The viewer draws it over the
|
||||
/// same area, so only detail beyond the GPU's texture limit is lost, instead of the upload
|
||||
/// failing (#201).
|
||||
pub fn fit_texture(img: egui::ColorImage, max_side: usize) -> egui::ColorImage {
|
||||
let [w, h] = img.size;
|
||||
let [fw, fh] = fitted_size(img.size, max_side);
|
||||
if [fw, fh] == [w, h] {
|
||||
return img;
|
||||
}
|
||||
let k = w.max(h).div_ceil(max_side.max(1)).max(1);
|
||||
let px: Vec<egui::Color32> = (0..fw * fh)
|
||||
.into_par_iter()
|
||||
.map(|i| {
|
||||
let (x0, y0) = ((i % fw) * k, (i / fw) * k);
|
||||
let (mut sum, mut n) = ([0u32; 4], 0u32);
|
||||
for y in y0..(y0 + k).min(h) {
|
||||
for c in img.pixels.get(y * w + x0..y * w + (x0 + k).min(w)).unwrap_or_default() {
|
||||
for (s, v) in sum.iter_mut().zip(c.to_array()) {
|
||||
*s += v as u32;
|
||||
}
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
let n = n.max(1);
|
||||
let [r, g, b, a] = sum.map(|s| ((s + n / 2) / n) as u8);
|
||||
egui::Color32::from_rgba_premultiplied(r, g, b, a)
|
||||
})
|
||||
.collect();
|
||||
egui::ColorImage::new([fw, fh], px)
|
||||
}
|
||||
|
||||
/// The largest texture side the renderer accepts.
|
||||
pub fn max_texture_side(ctx: &egui::Context) -> usize {
|
||||
ctx.input(|i| i.max_texture_side)
|
||||
}
|
||||
|
||||
/// `ctx.load_texture` for frame-sized images: fitted to the renderer's texture limit first.
|
||||
pub fn load_fitted(ctx: &egui::Context, name: impl Into<String>, img: egui::ColorImage, opts: egui::TextureOptions) -> egui::TextureHandle {
|
||||
ctx.load_texture(name, fit_texture(img, max_texture_side(ctx)), opts)
|
||||
}
|
||||
|
||||
impl Frames {
|
||||
/// Retire only GPU preview state; documents, CPU frames and running job leases survive.
|
||||
/// Publication checks the same latch while holding the cache lock, so a late GPU frame
|
||||
|
|
@ -1015,6 +1064,26 @@ impl Queue {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Frames larger than the texture limit are averaged down by a whole factor (#201: an
|
||||
/// 11000×2200 comp at Full resolution against an 8192 limit).
|
||||
#[test]
|
||||
fn frames_over_the_texture_limit_are_averaged_down() {
|
||||
assert_eq!(fitted_size([11000, 2200], 8192), [5500, 1100]);
|
||||
assert_eq!(fitted_size([8192, 100], 8192), [8192, 100]);
|
||||
assert_eq!(fitted_size([30000, 30000], 16384), [15000, 15000]);
|
||||
assert_eq!(fitted_size([0, 0], 0), [0, 0]);
|
||||
// 5×2 (three columns of white, then black) into 2 px: 3×2 blocks (the last one 2×2).
|
||||
let w = egui::Color32::WHITE;
|
||||
let b = egui::Color32::from_rgba_premultiplied(0, 0, 0, 255);
|
||||
let img = egui::ColorImage::new([5, 2], vec![w, w, w, b, b, w, w, w, b, b]);
|
||||
let out = fit_texture(img.clone(), 2);
|
||||
assert_eq!(out.size, [2, 1]);
|
||||
assert_eq!(out.pixels, vec![w, b]);
|
||||
assert_eq!(fit_texture(img.clone(), 5).pixels, img.pixels);
|
||||
let mixed = fit_texture(egui::ColorImage::new([2, 1], vec![w, b]), 1);
|
||||
assert_eq!(mixed.pixels, vec![egui::Color32::from_rgba_premultiplied(128, 128, 128, 255)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retirement_reaches_existing_workers_and_keeps_cpu_frames_and_completion_live() {
|
||||
let frames = Frames::default();
|
||||
|
|
|
|||
|
|
@ -576,6 +576,11 @@ impl EffectcraftApp {
|
|||
self.gpu.as_ref().map(effectcraft_engine::render::Accelerator::name)
|
||||
}
|
||||
|
||||
/// The size of the viewer frame's texture (CPU frames; fitted to the GPU's texture limit).
|
||||
pub fn viewer_texture_size(&self) -> Option<[usize; 2]> {
|
||||
self.viewer_tex.as_ref().map(|(t, _)| t.size())
|
||||
}
|
||||
|
||||
/// The viewer's current pixels as 8-bit premultiplied RGBA, reading a GPU frame back the
|
||||
/// first time something asks for it.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
Some((k, tex, size)) if k == key => Some((tex, size)),
|
||||
_ => app.session.footage.frame(view.item, f, view.time).map(|img| {
|
||||
let ci = egui::ColorImage::from_rgba_unmultiplied([img.width as usize, img.height as usize], &img.to_rgba8());
|
||||
let tex = ctx.load_texture("footage-panel", ci, egui::TextureOptions::LINEAR);
|
||||
let tex = crate::frames::load_fitted(&ctx, "footage-panel", ci, egui::TextureOptions::LINEAR);
|
||||
ctx.data_mut(|d| d.insert_temp(tid, (key, tex.clone(), [img.width, img.height])));
|
||||
(tex, [img.width, img.height])
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
let x0 = -buf.offset[0] / buf.scale;
|
||||
let y0 = -buf.offset[1] / buf.scale;
|
||||
let rect = [x0, y0, x0 + buf.img.width as f64 / buf.scale, y0 + buf.img.height as f64 / buf.scale];
|
||||
let tex = ctx.load_texture("layer-panel", img, egui::TextureOptions::LINEAR);
|
||||
let tex = crate::frames::load_fitted(&ctx, "layer-panel", img, egui::TextureOptions::LINEAR);
|
||||
let c = Tex { key, tex, rect };
|
||||
ctx.data_mut(|d| d.insert_temp(tex_id(), c.clone()));
|
||||
c
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ pub fn draw_overlay(
|
|||
let x0 = -inp.offset[0] / inp.scale;
|
||||
let y0 = -inp.offset[1] / inp.scale;
|
||||
let rect = [x0, y0, x0 + w as f64 / inp.scale, y0 + h as f64 / inp.scale];
|
||||
let tex = ctx.load_texture("roto-overlay", img, egui::TextureOptions::NEAREST);
|
||||
let tex = crate::frames::load_fitted(ctx, "roto-overlay", img, egui::TextureOptions::NEAREST);
|
||||
let c = Overlay { key, tex, rect };
|
||||
ctx.data_mut(|d| d.insert_temp(id, c.clone()));
|
||||
Some(c)
|
||||
|
|
|
|||
|
|
@ -397,7 +397,7 @@ fn aux_views(
|
|||
_ => {
|
||||
let opts = effectcraft_engine::render::RenderOpts { scale, view: comp.has_3d().then_some(cam), draft: true, ..Default::default() };
|
||||
let img = app.session.render(cid, t, opts);
|
||||
let tex = ctx.load_texture(format!("viewer-aux-{i}"), crate::frames::to_color_image(&img), egui::TextureOptions::LINEAR);
|
||||
let tex = crate::frames::load_fitted(&ctx, format!("viewer-aux-{i}"), crate::frames::to_color_image(&img), egui::TextureOptions::LINEAR);
|
||||
ctx.data_mut(|d| d.insert_temp(id, (key, tex.clone())));
|
||||
tex
|
||||
}
|
||||
|
|
@ -459,7 +459,7 @@ fn locked_pane(app: &mut EffectcraftApp, ui: &mut egui::Ui, full: Rect, bg: Colo
|
|||
dc.apply(&mut px);
|
||||
ci = egui::ColorImage::new(ci.size, px.into_iter().map(|a| Color32::from_rgba_premultiplied(a[0], a[1], a[2], a[3])).collect());
|
||||
}
|
||||
let tex = ctx.load_texture("viewer-locked", ci, egui::TextureOptions::LINEAR);
|
||||
let tex = crate::frames::load_fitted(&ctx, "viewer-locked", ci, egui::TextureOptions::LINEAR);
|
||||
ctx.data_mut(|d| d.insert_temp(id, (key, tex.clone())));
|
||||
tex
|
||||
}
|
||||
|
|
@ -1912,12 +1912,15 @@ fn show_frame(app: &mut EffectcraftApp, ctx: &egui::Context, key: crate::frames:
|
|||
let opts = zoom_texture_options(smooth);
|
||||
match img {
|
||||
FrameImage::Cpu(img) => {
|
||||
// Frames wider or taller than the GPU's texture limit go up averaged down (#201).
|
||||
let max = crate::frames::max_texture_side(ctx);
|
||||
let fitted = crate::frames::fit_texture((*img).clone(), max);
|
||||
match &mut app.viewer_tex {
|
||||
Some((tex, k)) if tex.size() == img.size => {
|
||||
tex.set((*img).clone(), opts);
|
||||
Some((tex, k)) if tex.size() == fitted.size => {
|
||||
tex.set(fitted, opts);
|
||||
*k = key;
|
||||
}
|
||||
_ => app.viewer_tex = Some((ctx.load_texture("viewer-frame", (*img).clone(), opts), key)),
|
||||
_ => app.viewer_tex = Some((ctx.load_texture("viewer-frame", fitted, opts), key)),
|
||||
}
|
||||
app.viewer_shown = app.viewer_tex.as_ref().map(|(t, k)| (t.id(), *k));
|
||||
app.viewer_image = Some(img);
|
||||
|
|
|
|||
|
|
@ -382,7 +382,7 @@ pub(crate) fn draw_frame(app: &mut EffectcraftApp, ctx: &egui::Context, painter:
|
|||
_ => {
|
||||
let img = crate::frames::to_color_image(&s.image);
|
||||
let img = if plain { img } else { transformed(&img, opts.channel, opts.colorized, opts.exposure, dc) };
|
||||
let tex = ctx.load_texture("viewer-snapshot", img, egui::TextureOptions::LINEAR);
|
||||
let tex = crate::frames::load_fitted(ctx, "viewer-snapshot", img, egui::TextureOptions::LINEAR);
|
||||
ctx.data_mut(|d| d.insert_temp(id, (key, tex.clone())));
|
||||
tex
|
||||
}
|
||||
|
|
@ -432,7 +432,7 @@ pub(crate) fn draw_frame(app: &mut EffectcraftApp, ctx: &egui::Context, painter:
|
|||
let tex = match ctx.data(|d| d.get_temp::<(u64, egui::TextureHandle)>(id)) {
|
||||
Some((k, tex)) if k == key => tex,
|
||||
_ => {
|
||||
let tex = ctx.load_texture("viewer-display", transformed(&src, opts.channel, opts.colorized, opts.exposure, dc), zoom_opts);
|
||||
let tex = crate::frames::load_fitted(ctx, "viewer-display", transformed(&src, opts.channel, opts.colorized, opts.exposure, dc), zoom_opts);
|
||||
ctx.data_mut(|d| d.insert_temp(id, (key, tex.clone())));
|
||||
tex
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,13 +131,15 @@ fn set_texture(app: &mut EffectcraftApp, ctx: &egui::Context, id: u32, key: Fram
|
|||
}
|
||||
let opts = super::viewer::zoom_texture_options(app.session.prefs.viewer_zoom_smooth());
|
||||
let tex = match (img, app.passive_tex.remove(&id)) {
|
||||
(FrameImage::Cpu(img), Some((_, PassiveTexture::Cpu(mut h)))) if h.size() == img.size => {
|
||||
h.set((*img).clone(), opts);
|
||||
(FrameImage::Cpu(img), Some((_, PassiveTexture::Cpu(mut h))))
|
||||
if h.size() == crate::frames::fitted_size(img.size, crate::frames::max_texture_side(ctx)) =>
|
||||
{
|
||||
h.set(crate::frames::fit_texture((*img).clone(), crate::frames::max_texture_side(ctx)), opts);
|
||||
PassiveTexture::Cpu(h)
|
||||
}
|
||||
(FrameImage::Cpu(img), old) => {
|
||||
free(app, old.map(|(_, t)| t));
|
||||
PassiveTexture::Cpu(ctx.load_texture(format!("viewer-{id}"), (*img).clone(), opts))
|
||||
PassiveTexture::Cpu(crate::frames::load_fitted(ctx, format!("viewer-{id}"), (*img).clone(), opts))
|
||||
}
|
||||
(FrameImage::Gpu(f), old) => {
|
||||
let Some(rs) = &app.wgpu else { return };
|
||||
|
|
|
|||
|
|
@ -108,10 +108,10 @@ fn video_preview(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
|||
let opts = crate::panels::viewer::zoom_texture_options(app.session.prefs.viewer_zoom_smooth());
|
||||
let t = match prev {
|
||||
Some((_, mut t)) => {
|
||||
t.set((*img).clone(), opts);
|
||||
t.set(crate::frames::fit_texture((*img).clone(), crate::frames::max_texture_side(ctx)), opts);
|
||||
t
|
||||
}
|
||||
None => ctx.load_texture("video-preview", (*img).clone(), opts),
|
||||
None => crate::frames::load_fitted(ctx, "video-preview", (*img).clone(), opts),
|
||||
};
|
||||
ctx.data_mut(|d| d.insert_temp(tex_id, (k, t.clone())));
|
||||
Some(t)
|
||||
|
|
|
|||
|
|
@ -1019,3 +1019,26 @@ fn handle_drags_scale_about_the_anchor_and_follow_the_pointer() {
|
|||
drag_path(&mut h, &path, egui::Modifiers::SHIFT);
|
||||
assert!(close(scale(&h), [162.5, 162.5]), "{:?}", scale(&h));
|
||||
}
|
||||
|
||||
/// A comp wider than the GPU's texture limit at Full resolution shows its frame (averaged down
|
||||
/// into a texture the renderer accepts) instead of failing the upload (#201: 11000×2200).
|
||||
#[test]
|
||||
fn full_resolution_frames_wider_than_the_texture_limit_fit() {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Wide", "width": 2400, "height": 400, "duration": 1})).unwrap();
|
||||
s.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
|
||||
// (egui's font atlas needs 1024.)
|
||||
h.input_mut().max_texture_side = Some(1024);
|
||||
h.state_mut().ui.viewer.res = effectcraft_ui_egui::state::Resolution::Full;
|
||||
let full = |h: &mut Harness<'_, EffectcraftApp>| h.state_mut().viewer_pixels().is_some_and(|px| px.size == [2400, 400]);
|
||||
for _ in 0..400 {
|
||||
h.step();
|
||||
if full(&mut h) {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
assert!(full(&mut h), "the full-size frame is shown (and stays readable): {:?}", h.state().ui.viewer.res);
|
||||
assert_eq!(h.state().viewer_texture_size(), Some([800, 134]), "2400×400 averaged by 3");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -491,7 +491,8 @@ not read or written.
|
|||
|
||||
## Seeing the UI
|
||||
|
||||
To work on the UI, start the app with `cargo run -p effectcraft -- --control 9877` and use MCP bridge
|
||||
To work on the UI, start the app with `cargo run -p effectcraft -- --control 9877` (add `--demo` to
|
||||
open the demo project; without it the app starts with an empty project) and use MCP bridge
|
||||
mode or the raw control channel. A good loop is: `ui_elements` to find an id, `ui_click` or `ui_drag`
|
||||
to act, then `screenshot {"panel":"Timeline"}` to check the result. `render_frame` shows the
|
||||
composition itself at any zoom.
|
||||
|
|
|
|||
Loading…
Reference in New Issue