1use crate::engine::shaders;
30use crate::media::Frame;
31use crate::model::{BlendMode, Clip, ClipKind, Effect, EffectKind, Id, Mask, MaskShape, NodeGraph, NodeKind};
32use crate::model::{Project, Scaler, TrackKind};
33use eframe::glow;
34use eframe::glow::HasContext;
35use std::collections::HashMap;
36use std::num::NonZeroU32;
37use std::sync::Arc;
38
39pub struct Target {
41 pub tex: glow::Texture,
42 pub fbo: glow::Framebuffer,
43 pub w: u32,
44 pub h: u32,
45}
46
47#[derive(Default)]
49pub struct Programs {
50 map: HashMap<u64, Prog>,
51 failed: HashMap<u64, String>,
53}
54
55struct Prog {
56 p: glow::Program,
57 uni: HashMap<&'static str, glow::UniformLocation>,
58}
59
60const UNIFORMS: &[&str] = &[
62 "tex",
63 "u_res",
64 "u_time",
65 "u_scale",
66 "p0",
67 "p1",
68 "p2",
69 "p3",
70 "p4",
71 "p5",
72 "p6",
73 "p7",
74 "p8",
76 "p9",
77 "p10",
78 "p11",
79 "u_mask",
80 "u_has_mask",
81 "u_dst",
82 "u_layer",
83 "u_inv",
84 "u_scaler",
85 "u_mode",
86 "u_opacity",
87 "u_b",
88 "u_matte_alpha",
89 "u_matte_invert",
90 "u_prev",
91 "u_next",
92 "u_frames",
93 "u_motion",
94 "m_shape",
95 "m_center",
96 "m_radius",
97 "m_rot",
98 "m_feather",
99 "m_expand",
100 "m_opacity",
101 "m_invert",
102 "m_count",
103];
104
105const K_COMPOSITE: u64 = 1;
108const K_MASK: u64 = 2;
109const K_COPY: u64 = 3;
110const K_MATTE: u64 = 4;
111const EFFECT_BASE: u64 = 16;
112const USER_BIT: u64 = 1 << 63;
113
114const COPY_BODY: &str = "vec4 effect(vec4 src, vec2 uv) { return src; }";
116const MATTE_BODY: &str = r#"
117uniform sampler2D u_b;
118uniform int u_matte_alpha;
119uniform int u_matte_invert;
120vec4 effect(vec4 src, vec2 uv) {
121 vec4 m = texture(u_b, uv);
122 float k = (u_matte_alpha != 0) ? m.a : luma(m.rgb) * m.a;
123 if (u_matte_invert != 0) { k = 1.0 - k; }
124 return vec4(src.rgb, src.a * clamp(k, 0.0, 1.0));
125}
126"#;
127
128const U_TEX: u32 = 0;
130const U_MASK: u32 = 1;
131const U_DST: u32 = 2;
132const U_B: u32 = 3;
133const U_PREV: u32 = 4;
134const U_NEXT: u32 = 5;
135
136const MOTION_PREV: u64 = 1 << 62;
139const MOTION_NEXT: u64 = 1 << 61;
140
141struct LayerTex {
143 tex: glow::Texture,
144 w: u32,
145 h: u32,
146 rev: (usize, u64),
147}
148
149#[derive(Default)]
154struct Pool {
155 gl: Option<Arc<glow::Context>>,
157 free: Vec<Target>,
158 live: usize,
160}
161
162impl Pool {
163 const MAX_FREE_PX: usize = 64 << 20;
166
167 fn take(&mut self, w: u32, h: u32) -> Option<Target> {
169 let i = self.free.iter().position(|t| t.w == w && t.h == h)?;
170 self.live += 1;
171 Some(self.free.swap_remove(i))
172 }
173 fn created(&mut self) {
175 self.live += 1;
176 }
177 fn put(&mut self, t: Target) {
178 self.live = self.live.saturating_sub(1);
179 self.free.push(t);
180 let px = |t: &Target| t.w as usize * t.h as usize;
183 let mut total: usize = self.free.iter().map(px).sum();
184 while total > Self::MAX_FREE_PX && self.free.len() > 1 {
185 let old = self.free.remove(0);
186 total -= px(&old);
187 if let Some(gl) = &self.gl {
188 unsafe {
189 gl.delete_framebuffer(old.fbo);
190 gl.delete_texture(old.tex);
191 }
192 }
193 }
194 }
195}
196
197pub struct GpuRenderer {
199 gl: Arc<glow::Context>,
200 vao: glow::VertexArray,
201 vert: glow::Shader,
202 programs: Programs,
203 pool: Pool,
204 textures: HashMap<u64, LayerTex>,
206 blank: glow::Texture,
208 loaned: Vec<Target>,
210 pub text: Option<Arc<std::sync::Mutex<crate::engine::text::TextRasterizer>>>,
213}
214
215impl GpuRenderer {
216 pub fn new(gl: Arc<glow::Context>) -> Result<Self, String> {
219 unsafe {
220 let vao = gl.create_vertex_array().map_err(|e| format!("vertex array: {e}"))?;
221 let vert = compile(&gl, glow::VERTEX_SHADER, shaders::VERT)?;
222 let blank = gl.create_texture().map_err(|e| format!("texture: {e}"))?;
223 gl.bind_texture(glow::TEXTURE_2D, Some(blank));
224 tex_params(&gl);
225 gl.tex_image_2d(
226 glow::TEXTURE_2D,
227 0,
228 glow::RGBA8 as i32,
229 1,
230 1,
231 0,
232 glow::RGBA,
233 glow::UNSIGNED_BYTE,
234 glow::PixelUnpackData::Slice(Some(&[255, 255, 255, 255])),
235 );
236 let mut r = Self {
237 pool: Pool { gl: Some(gl.clone()), ..Pool::default() },
238 gl,
239 vao,
240 vert,
241 programs: Programs::default(),
242 textures: HashMap::new(),
243 blank,
244 loaned: Vec::new(),
245 text: None,
246 };
247 r.ensure(K_COMPOSITE)?;
250 Ok(r)
251 }
252 }
253
254 pub fn upload(&mut self, key: u64, frame: &Frame) -> glow::Texture {
257 if frame.is_empty() {
258 return self.blank;
259 }
260 let gl = self.gl.clone();
261 let rev = (frame.rgba.as_ptr() as usize, frame.pts.to_bits());
262 let (w, h) = (frame.width, frame.height);
263 unsafe {
264 match self.textures.get_mut(&key) {
265 Some(lt) if lt.w == w && lt.h == h => {
266 if lt.rev == rev {
267 return lt.tex;
268 }
269 lt.rev = rev;
270 gl.bind_texture(glow::TEXTURE_2D, Some(lt.tex));
271 gl.tex_sub_image_2d(
272 glow::TEXTURE_2D,
273 0,
274 0,
275 0,
276 w as i32,
277 h as i32,
278 glow::RGBA,
279 glow::UNSIGNED_BYTE,
280 glow::PixelUnpackData::Slice(Some(&frame.rgba)),
281 );
282 return lt.tex;
283 }
284 _ => {}
285 }
286 if let Some(old) = self.textures.remove(&key) {
287 gl.delete_texture(old.tex);
288 }
289 let Ok(tex) = gl.create_texture() else { return self.blank };
290 gl.bind_texture(glow::TEXTURE_2D, Some(tex));
291 tex_params(&gl);
292 gl.tex_image_2d(
293 glow::TEXTURE_2D,
294 0,
295 glow::RGBA8 as i32,
296 w as i32,
297 h as i32,
298 0,
299 glow::RGBA,
300 glow::UNSIGNED_BYTE,
301 glow::PixelUnpackData::Slice(Some(&frame.rgba)),
302 );
303 self.textures.insert(key, LayerTex { tex, w, h, rev });
304 tex
305 }
306 }
307
308 pub fn render_to_texture(
312 &mut self,
313 project: &Project,
314 t: f64,
315 w: u32,
316 h: u32,
317 quality: f32,
318 layers: &LayerSet,
319 ) -> Option<glow::Texture> {
320 self.reclaim();
321 let host = self.host_fbo();
322 let (rw, rh) = render_size(w, h, quality);
323 let canvas = self.render_canvas(project, t, rw, rh, layers);
324 self.restore(host);
325 let canvas = canvas?;
326 let tex = canvas.tex;
327 self.loaned.push(canvas);
328 Some(tex)
329 }
330
331 pub fn render_frame(&mut self, project: &Project, t: f64, w: u32, h: u32, layers: &LayerSet, out: &mut Frame) {
333 self.reclaim();
334 let host = self.host_fbo();
335 out.resize(w, h);
336 out.pts = t;
337 if w == 0 || h == 0 {
338 return;
339 }
340 let Some(canvas) = self.render_canvas(project, t, w, h, layers) else {
341 self.restore(host);
342 return;
343 };
344 let gl = self.gl.clone();
345 unsafe {
346 gl.bind_framebuffer(glow::FRAMEBUFFER, Some(canvas.fbo));
347 gl.pixel_store_i32(glow::PACK_ALIGNMENT, 1);
348 gl.read_pixels(
349 0,
350 0,
351 w as i32,
352 h as i32,
353 glow::RGBA,
354 glow::UNSIGNED_BYTE,
355 glow::PixelPackData::Slice(Some(&mut out.rgba)),
356 );
357 }
358 self.pool.put(canvas);
359 self.restore(host);
360 }
361
362 pub fn render_preview_texture(
367 &mut self,
368 project: &Project,
369 t: f64,
370 w: u32,
371 h: u32,
372 layers: &LayerSet,
373 ) -> Option<(glow::Texture, u32, u32)> {
374 if w == 0 || h == 0 {
375 return None;
376 }
377 self.reclaim();
378 let host = self.host_fbo();
379 let canvas = self.render_canvas(project, t, w, h, layers);
380 self.restore(host);
381 let canvas = canvas?;
382 let out = (canvas.tex, canvas.w, canvas.h);
383 self.loaned.push(canvas);
385 Some(out)
386 }
387
388 pub fn effect_preview(&mut self, src: &Frame, effect: &Effect, t: f64, out: &mut Frame) -> bool {
391 if src.is_empty() {
392 return false;
393 }
394 let (w, h) = (src.width, src.height);
395 let host = self.host_fbo();
396 let tex = self.upload(u64::MAX, src);
397 let blob = (effect.kind == EffectKind::BlobTrack)
398 .then(|| crate::engine::effects::track(src, ¶m_values(effect, t)))
399 .flatten();
400 let dst = self.run_effect(tex, (w, h), effect, t, 1.0, None, None, blob);
401 let ok = match dst {
402 Some(target) => {
403 out.resize(w, h);
404 let gl = self.gl.clone();
405 unsafe {
406 gl.bind_framebuffer(glow::FRAMEBUFFER, Some(target.fbo));
407 gl.pixel_store_i32(glow::PACK_ALIGNMENT, 1);
408 gl.read_pixels(
409 0,
410 0,
411 w as i32,
412 h as i32,
413 glow::RGBA,
414 glow::UNSIGNED_BYTE,
415 glow::PixelPackData::Slice(Some(&mut out.rgba)),
416 );
417 }
418 self.pool.put(target);
419 true
420 }
421 None => false,
422 };
423 self.restore(host);
424 ok
425 }
426
427 pub fn apply_effect(
428 &mut self,
429 src: glow::Texture,
430 size: (u32, u32),
431 effect: &Effect,
432 t: f64,
433 ) -> Option<glow::Texture> {
434 let host = self.host_fbo();
435 let dst = self.run_effect(src, size, effect, t, 1.0, None, None, None);
436 self.restore(host);
437 let dst = dst?;
438 let tex = dst.tex;
439 self.loaned.push(dst);
440 Some(tex)
441 }
442
443 pub fn mask_texture(&mut self, mask: &Mask, size: (u32, u32), t: f64, project_w: u32) -> Option<glow::Texture> {
445 let scale = size.0 as f32 / project_w.max(1) as f32;
446 let centre = (size.0 as f32 / 2.0, size.1 as f32 / 2.0);
447 let host = self.host_fbo();
448 let target = self.render_mask(mask, size, t, scale, centre);
449 self.restore(host);
450 let target = target?;
451 let tex = target.tex;
452 self.loaned.push(target);
453 Some(tex)
454 }
455
456 pub fn eval_graph(
458 &mut self,
459 graph: &NodeGraph,
460 clip: &Clip,
461 t: f64,
462 fps: f64,
463 layers: &LayerSet,
464 ) -> Option<glow::Texture> {
465 let input = layers.get(clip.id).map(|f| (self.upload(clip.id, f), f.width, f.height));
466 let size = input.map(|(_, w, h)| (w, h)).filter(|s| s.0 > 0 && s.1 > 0)?;
467 let host = self.host_fbo();
468 let out = self.eval_graph_on(graph, clip, t, fps, layers, input.map(|(tex, ..)| tex), size, 1.0);
469 self.restore(host);
470 let out = out?;
471 let tex = out.tex;
472 self.loaned.push(out);
473 Some(tex)
474 }
475
476 pub fn invalidate(&mut self, key: Option<u64>) {
478 let gl = self.gl.clone();
479 unsafe {
480 match key {
481 Some(k) => {
482 if let Some(t) = self.textures.remove(&k) {
483 gl.delete_texture(t.tex);
484 }
485 }
486 None => {
487 for (_, t) in self.textures.drain() {
488 gl.delete_texture(t.tex);
489 }
490 }
491 }
492 }
493 }
494
495 pub fn scaler_supported(&self, _s: Scaler) -> bool {
496 true
497 }
498
499 pub fn last_error(&self) -> Option<&str> {
501 self.programs.failed.values().next().map(|s| s.as_str())
502 }
503
504 pub fn shader_error(&self, src: &str) -> Option<&str> {
506 self.programs.failed.get(&user_key(src)).map(|s| s.as_str())
507 }
508
509 pub fn check_shader(&mut self, src: &str) -> Result<(), String> {
512 let key = user_key(src);
513 if self.programs.map.contains_key(&key) {
514 return Ok(());
515 }
516 if let Some(e) = self.programs.failed.get(&key) {
517 return Err(e.clone());
518 }
519 self.link(key, &shaders::user_shader(src))
520 }
521
522 fn host_fbo(&self) -> Option<glow::Framebuffer> {
526 unsafe {
527 NonZeroU32::new(self.gl.get_parameter_i32(glow::DRAW_FRAMEBUFFER_BINDING) as u32)
528 .map(glow::NativeFramebuffer)
529 }
530 }
531
532 fn restore(&self, host: Option<glow::Framebuffer>) {
534 unsafe {
535 self.gl.bind_framebuffer(glow::FRAMEBUFFER, host);
536 self.gl.active_texture(glow::TEXTURE0);
537 self.gl.bind_texture(glow::TEXTURE_2D, None);
538 self.gl.use_program(None);
539 }
540 }
541
542 fn reclaim(&mut self) {
544 for t in std::mem::take(&mut self.loaned) {
547 self.pool.put(t);
548 }
549 }
550
551 fn acquire(&mut self, w: u32, h: u32) -> Option<Target> {
552 if w == 0 || h == 0 {
553 return None;
554 }
555 if let Some(t) = self.pool.take(w, h) {
556 return Some(t);
557 }
558 let gl = self.gl.clone();
559 unsafe {
560 let tex = gl.create_texture().ok()?;
561 gl.bind_texture(glow::TEXTURE_2D, Some(tex));
562 tex_params(&gl);
563 gl.tex_image_2d(
564 glow::TEXTURE_2D,
565 0,
566 glow::RGBA8 as i32,
567 w as i32,
568 h as i32,
569 0,
570 glow::RGBA,
571 glow::UNSIGNED_BYTE,
572 glow::PixelUnpackData::Slice(None),
573 );
574 let fbo = gl.create_framebuffer().ok()?;
575 gl.bind_framebuffer(glow::FRAMEBUFFER, Some(fbo));
576 gl.framebuffer_texture_2d(glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0, glow::TEXTURE_2D, Some(tex), 0);
577 if gl.check_framebuffer_status(glow::FRAMEBUFFER) != glow::FRAMEBUFFER_COMPLETE {
578 gl.delete_framebuffer(fbo);
579 gl.delete_texture(tex);
580 gl.bind_framebuffer(glow::FRAMEBUFFER, None);
581 return None;
582 }
583 gl.bind_framebuffer(glow::FRAMEBUFFER, None);
584 self.pool.created();
585 Some(Target { tex, fbo, w, h })
586 }
587 }
588
589 fn clear(&self, t: &Target, c: [f32; 4]) {
590 let gl = &self.gl;
591 unsafe {
592 gl.bind_framebuffer(glow::FRAMEBUFFER, Some(t.fbo));
593 gl.disable(glow::SCISSOR_TEST);
594 gl.disable(glow::BLEND);
595 gl.clear_color(c[0], c[1], c[2], c[3]);
596 gl.clear(glow::COLOR_BUFFER_BIT);
597 }
598 }
599
600 fn ensure(&mut self, key: u64) -> Result<(), String> {
602 if self.programs.map.contains_key(&key) {
603 return Ok(());
604 }
605 if let Some(e) = self.programs.failed.get(&key) {
606 return Err(e.clone());
607 }
608 let src = match key {
609 K_COMPOSITE => format!("{}{}{}", shaders::PRELUDE, shaders::BLEND, shaders::COMPOSITE),
610 K_MASK => format!("{}{}", shaders::PRELUDE, shaders::MASK),
611 K_COPY => format!("{}\n{}\n{}", shaders::PRELUDE, COPY_BODY, shaders::MAIN),
612 K_MATTE => format!("{}\n{}\n{}", shaders::PRELUDE, MATTE_BODY, shaders::MAIN),
613 _ => return Err("no source for this program".into()),
614 };
615 self.link(key, &src)
616 }
617
618 fn link(&mut self, key: u64, src: &str) -> Result<(), String> {
619 let gl = self.gl.clone();
620 let r = unsafe { build(&gl, self.vert, src) };
621 match r {
622 Ok(p) => {
623 self.programs.map.insert(key, p);
624 Ok(())
625 }
626 Err(e) => {
627 if cfg!(debug_assertions) {
630 eprintln!("GLSL program {key:#x} failed to build:\n{e}");
631 }
632 self.programs.failed.insert(key, e.clone());
633 Err(e)
634 }
635 }
636 }
637
638 fn effect_program(&mut self, effect: &Effect) -> Option<u64> {
640 let key =
641 if effect.kind == EffectKind::Shader { user_key(&effect.shader) } else { EFFECT_BASE + effect.kind as u64 };
642 if self.programs.map.contains_key(&key) {
643 return Some(key);
644 }
645 if self.programs.failed.contains_key(&key) {
646 return None;
647 }
648 let src = shaders::fragment(effect.kind, &effect.shader)?;
649 self.link(key, &src).ok().map(|_| key)
650 }
651
652 fn draw(&self, key: u64, dst: &Target, tex: glow::Texture, mask: Option<glow::Texture>, set: impl Fn(&Prog)) {
654 let Some(prog) = self.programs.map.get(&key) else { return };
655 let gl = &self.gl;
656 unsafe {
657 gl.bind_framebuffer(glow::FRAMEBUFFER, Some(dst.fbo));
658 gl.viewport(0, 0, dst.w as i32, dst.h as i32);
659 gl.disable(glow::BLEND);
660 gl.disable(glow::SCISSOR_TEST);
661 gl.disable(glow::DEPTH_TEST);
662 gl.use_program(Some(prog.p));
663 gl.active_texture(glow::TEXTURE0 + U_TEX);
664 gl.bind_texture(glow::TEXTURE_2D, Some(tex));
665 gl.uniform_1_i32(prog.uni.get("tex"), U_TEX as i32);
666 gl.active_texture(glow::TEXTURE0 + U_MASK);
667 gl.bind_texture(glow::TEXTURE_2D, Some(mask.unwrap_or(self.blank)));
668 gl.uniform_1_i32(prog.uni.get("u_mask"), U_MASK as i32);
669 gl.uniform_1_i32(prog.uni.get("u_has_mask"), i32::from(mask.is_some()));
670 set(prog);
671 gl.bind_vertex_array(Some(self.vao));
672 gl.draw_arrays(glow::TRIANGLES, 0, 3);
673 gl.bind_vertex_array(None);
674 }
675 }
676
677 fn copy_to(&mut self, src: glow::Texture, size: (u32, u32)) -> Option<Target> {
679 self.ensure(K_COPY).ok()?;
680 let dst = self.acquire(size.0, size.1)?;
681 let gl = self.gl.clone();
682 self.draw(K_COPY, &dst, src, None, |p| unsafe {
683 gl.uniform_2_f32(p.uni.get("u_res"), size.0 as f32, size.1 as f32);
684 });
685 Some(dst)
686 }
687
688 #[allow(clippy::too_many_arguments)]
692 fn run_effect(
693 &mut self,
694 src: glow::Texture,
695 size: (u32, u32),
696 effect: &Effect,
697 t: f64,
698 scale: f32,
699 mask: Option<glow::Texture>,
700 motion: Option<(glow::Texture, glow::Texture, f32)>,
701 blob: Option<(f64, f64, f64)>,
703 ) -> Option<Target> {
704 let key = self.effect_program(effect)?;
705 let dst = self.acquire(size.0, size.1)?;
706 let gl = self.gl.clone();
707 let mut params = [0.0f32; PARAM_NAMES.len()];
708 for (i, p) in params.iter_mut().enumerate() {
709 *p = effect.at(i, t) as f32;
710 }
711 let motion = effect.kind.needs_motion().then(|| motion.unwrap_or((self.blank, self.blank, 0.0)));
713 self.draw(key, &dst, src, mask, |prog| unsafe {
714 gl.uniform_2_f32(prog.uni.get("u_res"), size.0 as f32, size.1 as f32);
715 gl.uniform_1_f32(prog.uni.get("u_time"), t as f32);
716 gl.uniform_1_f32(prog.uni.get("u_scale"), scale);
717 for (i, v) in params.iter().enumerate() {
718 gl.uniform_1_f32(prog.uni.get(PARAM_NAMES[i]), *v);
719 }
720 if let Some((prev, next, frames)) = motion {
721 gl.active_texture(glow::TEXTURE0 + U_PREV);
722 gl.bind_texture(glow::TEXTURE_2D, Some(prev));
723 gl.uniform_1_i32(prog.uni.get("u_prev"), U_PREV as i32);
724 gl.active_texture(glow::TEXTURE0 + U_NEXT);
725 gl.bind_texture(glow::TEXTURE_2D, Some(next));
726 gl.uniform_1_i32(prog.uni.get("u_next"), U_NEXT as i32);
727 gl.uniform_1_f32(prog.uni.get("u_frames"), frames);
728 gl.uniform_2_f32(prog.uni.get("u_motion"), 0.0, 0.0);
731 }
732 if effect.kind == EffectKind::BlobTrack {
733 let (cx, cy, area) = blob.unwrap_or((0.5, 0.5, 0.0));
735 gl.uniform_3_f32(prog.uni.get("u_blob"), cx as f32, cy as f32, area as f32);
736 }
737 });
738 Some(dst)
739 }
740
741 fn track_blob(&self, effect: &Effect, clip: Id, t: f64, layers: &LayerSet) -> Option<(f64, f64, f64)> {
744 if effect.kind != EffectKind::BlobTrack {
745 return None;
746 }
747 let frame = layers.get(clip)?;
748 crate::engine::effects::track(frame, ¶m_values(effect, t))
749 }
750
751 fn motion_texs(&mut self, id: Id, layers: &LayerSet) -> (glow::Texture, glow::Texture, f32) {
754 let (mut before, mut after) = (None, None);
755 let (mut bo, mut ao) = (0.0f64, 0.0f64);
756 for (cid, off, f) in &layers.motion {
757 if *cid != id || f.is_empty() {
758 continue;
759 }
760 if *off < bo {
761 bo = *off;
762 before = Some(f);
763 } else if *off > ao {
764 ao = *off;
765 after = Some(f);
766 }
767 }
768 match (before, after) {
769 (None, None) => (self.blank, self.blank, 0.0),
770 (Some(f), None) => {
771 let tex = self.upload(id | MOTION_PREV, f);
772 (tex, tex, 1.0)
773 }
774 (None, Some(f)) => {
775 let tex = self.upload(id | MOTION_NEXT, f);
776 (tex, tex, 1.0)
777 }
778 (Some(p), Some(n)) => {
779 let prev = self.upload(id | MOTION_PREV, p);
780 let next = self.upload(id | MOTION_NEXT, n);
781 (prev, next, 2.0)
782 }
783 }
784 }
785
786 fn render_mask(&mut self, mask: &Mask, size: (u32, u32), t: f64, scale: f32, centre: (f32, f32)) -> Option<Target> {
788 self.ensure(K_MASK).ok()?;
789 let dst = self.acquire(size.0, size.1)?;
790 let gl = self.gl.clone();
791 let mut pts = Vec::with_capacity(mask.points.len().min(MAX_MASK_POINTS) * 2);
792 for (x, y) in mask.points.iter().take(MAX_MASK_POINTS) {
793 pts.push(centre.0 + x * scale);
794 pts.push(centre.1 + y * scale);
795 }
796 let n = (pts.len() / 2) as i32;
797 let shape = match mask.shape {
798 MaskShape::Rect => 0,
799 MaskShape::Ellipse => 1,
800 MaskShape::Polygon | MaskShape::Path => 2,
803 };
804 let (cx, cy) = (centre.0 + mask.cx.at(t) as f32 * scale, centre.1 + mask.cy.at(t) as f32 * scale);
805 let (rx, ry) = (mask.rx.at(t) as f32 * scale, mask.ry.at(t) as f32 * scale);
806 self.draw(K_MASK, &dst, self.blank, None, |p| unsafe {
807 gl.uniform_2_f32(p.uni.get("u_res"), size.0 as f32, size.1 as f32);
808 gl.uniform_1_i32(p.uni.get("m_shape"), shape);
809 gl.uniform_2_f32(p.uni.get("m_center"), cx, cy);
810 gl.uniform_2_f32(p.uni.get("m_radius"), rx.abs(), ry.abs());
811 gl.uniform_1_f32(p.uni.get("m_rot"), mask.rotation.at(t) as f32);
812 gl.uniform_1_f32(p.uni.get("m_feather"), (mask.feather.at(t) as f32 * scale).max(0.0));
813 gl.uniform_1_f32(p.uni.get("m_expand"), mask.expand.at(t) as f32 * scale);
814 gl.uniform_1_f32(p.uni.get("m_opacity"), mask.opacity.at(t).clamp(0.0, 1.0) as f32);
815 gl.uniform_1_i32(p.uni.get("m_invert"), i32::from(mask.invert));
816 gl.uniform_1_i32(p.uni.get("m_count"), n);
817 if !pts.is_empty() {
818 gl.uniform_2_f32_slice(p.uni.get("m_points"), &pts);
819 }
820 });
821 Some(dst)
822 }
823
824 #[allow(clippy::too_many_arguments)]
827 fn composite(
828 &mut self,
829 canvas: Target,
830 layer: glow::Texture,
831 layer_size: (u32, u32),
832 inv: [[f32; 3]; 3],
833 mode: BlendMode,
834 opacity: f32,
835 scaler: Scaler,
836 mask: Option<glow::Texture>,
837 ) -> Target {
838 if self.ensure(K_COMPOSITE).is_err() {
839 return canvas;
840 }
841 let Some(dst) = self.acquire(canvas.w, canvas.h) else { return canvas };
842 let gl = self.gl.clone();
843 let canvas_tex = canvas.tex;
844 let m = column_major(&inv);
845 let (cw, ch) = (canvas.w as f32, canvas.h as f32);
846 let mode_i = blend_index(mode);
847 let scaler_i = scaler_index(scaler);
848 self.draw(K_COMPOSITE, &dst, layer, mask, |p| unsafe {
849 gl.uniform_2_f32(p.uni.get("u_res"), cw, ch);
850 gl.uniform_2_f32(p.uni.get("u_layer"), layer_size.0 as f32, layer_size.1 as f32);
851 gl.uniform_matrix_3_f32_slice(p.uni.get("u_inv"), false, &m);
852 gl.uniform_1_i32(p.uni.get("u_scaler"), scaler_i);
853 gl.uniform_1_i32(p.uni.get("u_mode"), mode_i);
854 gl.uniform_1_f32(p.uni.get("u_opacity"), opacity);
855 gl.active_texture(glow::TEXTURE0 + U_DST);
856 gl.bind_texture(glow::TEXTURE_2D, Some(canvas_tex));
857 gl.uniform_1_i32(p.uni.get("u_dst"), U_DST as i32);
858 });
859 self.pool.put(canvas);
860 dst
861 }
862
863 fn render_canvas(&mut self, project: &Project, t: f64, w: u32, h: u32, layers: &LayerSet) -> Option<Target> {
865 let mut canvas = self.acquire(w, h)?;
866 self.clear(&canvas, [0.0, 0.0, 0.0, 1.0]);
867 for (ti, track) in project.tracks.iter().enumerate() {
868 if track.kind != TrackKind::Video || !project.active(ti) {
869 continue;
870 }
871 if let Some((tr, left, right)) = track.transition_at(t) {
872 let p = crate::engine::compose::trans_progress(tr, left, right, t) as f32;
873 canvas = self.draw_transition(project, tr, left, right, p, t, canvas, layers);
874 continue;
875 }
876 for clip in &track.clips {
877 if !clip.enabled || !clip.contains(t) {
878 continue;
879 }
880 canvas = self.draw_clip(project, clip, t, canvas, layers, Extra::NONE);
881 }
882 }
883 if project.show_subtitles {
884 if let Some(sub) = layers.get(LayerSet::SUBTITLES) {
885 canvas = self.draw_subtitles(project, sub, canvas);
886 }
887 }
888 Some(canvas)
889 }
890
891 #[allow(clippy::too_many_arguments)]
894 fn draw_transition(
895 &mut self,
896 project: &Project,
897 tr: &crate::model::Transition,
898 left: Option<&Clip>,
899 right: Option<&Clip>,
900 p: f32,
901 t: f64,
902 mut canvas: Target,
903 layers: &LayerSet,
904 ) -> Target {
905 use crate::model::TransitionKind::*;
906 let (w, h) = (canvas.w as f32, canvas.h as f32);
907 match tr.kind {
908 CrossFade => {
909 if let Some(left) = left {
910 let op = if right.is_none() { 1.0 - p } else { 1.0 };
912 canvas = self.draw_clip(project, left, t, canvas, layers, Extra { opacity: op, ..Extra::NONE });
913 }
914 if let Some(right) = right {
915 canvas = self.draw_clip(project, right, t, canvas, layers, Extra { opacity: p, ..Extra::NONE });
916 }
917 canvas
918 }
919 FadeToColor => {
920 let (clip, fade) = match (left, right) {
922 (Some(l), Some(r)) => {
923 if p < 0.5 {
924 (l, 2.0 * p)
925 } else {
926 (r, 2.0 * (1.0 - p))
927 }
928 }
929 (Some(l), None) => (l, p),
930 (None, Some(r)) => (r, 1.0 - p),
931 (None, None) => return canvas,
932 };
933 canvas = self.draw_clip(project, clip, t, canvas, layers, Extra::NONE);
934 self.fill_canvas(canvas, tr.color, fade.clamp(0.0, 1.0))
935 }
936 Push => {
937 let (dx, dy) = match tr.direction {
938 0 => (-w, 0.0),
939 1 => (w, 0.0),
940 2 => (0.0, -h),
941 _ => (0.0, h),
942 };
943 if let Some(left) = left {
944 let a = Extra { dx: -p * dx, dy: -p * dy, ..Extra::NONE };
945 canvas = self.draw_clip(project, left, t, canvas, layers, a);
946 }
947 if let Some(right) = right {
948 let b = Extra { dx: (1.0 - p) * dx, dy: (1.0 - p) * dy, ..Extra::NONE };
949 canvas = self.draw_clip(project, right, t, canvas, layers, b);
950 }
951 canvas
952 }
953 Wipe => {
954 let (clip, d, q) = match (left, right) {
958 (l, Some(r)) => {
959 if let Some(l) = l {
960 canvas = self.draw_clip(project, l, t, canvas, layers, Extra::NONE);
961 }
962 (r, tr.direction, p)
963 }
964 (Some(l), None) => (l, tr.direction ^ 1, 1.0 - p),
965 (None, None) => return canvas,
966 };
967 let (cx, cy, rx, ry) = match d {
968 0 => (w * q / 2.0, h / 2.0, w * q / 2.0, h / 2.0),
969 1 => (w - w * q / 2.0, h / 2.0, w * q / 2.0, h / 2.0),
970 2 => (w / 2.0, h * q / 2.0, w / 2.0, h * q / 2.0),
971 _ => (w / 2.0, h - h * q / 2.0, w / 2.0, h * q / 2.0),
972 };
973 let mut rect = Mask::new(MaskShape::Rect);
974 rect.rx = crate::model::Animated::new(rx as f64);
975 rect.ry = crate::model::Animated::new(ry as f64);
976 let size = (canvas.w, canvas.h);
977 let Some(mask) = self.render_mask(&rect, size, 0.0, 1.0, (cx, cy)) else { return canvas };
978 let extra = Extra { mask: Some(mask.tex), ..Extra::NONE };
979 let out = self.draw_clip(project, clip, t, canvas, layers, extra);
980 self.pool.put(mask);
981 out
982 }
983 }
984 }
985
986 fn fill_canvas(&mut self, canvas: Target, color: [u8; 4], f: f32) -> Target {
988 if f <= 0.0 {
989 return canvas;
990 }
991 let Some(fill) = self.acquire(1, 1) else { return canvas };
992 self.clear(&fill, [color[0] as f32 / 255.0, color[1] as f32 / 255.0, color[2] as f32 / 255.0, 1.0]);
993 let inv = [[1.0 / canvas.w as f32, 0.0, 0.0], [0.0, 1.0 / canvas.h as f32, 0.0], [0.0, 0.0, 1.0]];
994 let out = self.composite(canvas, fill.tex, (1, 1), inv, BlendMode::Normal, f.min(1.0), Scaler::Nearest, None);
995 self.pool.put(fill);
996 out
997 }
998
999 fn draw_subtitles(&mut self, project: &Project, sub: &Arc<Frame>, canvas: Target) -> Target {
1001 if sub.is_empty() {
1002 return canvas;
1003 }
1004 let (w, h) = (canvas.w, canvas.h);
1005 let sv = h as f32 / project.height.max(1) as f32;
1006 let p = crate::engine::compose::Placement {
1007 cx: w as f32 / 2.0,
1008 cy: h as f32 - project.subtitle_margin * sv - sub.height as f32 / 2.0,
1009 w: sub.width as f32,
1010 h: sub.height as f32,
1011 rot: 0.0,
1012 yaw: 0.0,
1013 pitch: 0.0,
1014 };
1015 let Some(inv) = inverse_placement(&p, w as f32, 0.0) else { return canvas };
1016 let tex = self.upload(LayerSet::SUBTITLES, sub);
1017 let size = (sub.width, sub.height);
1018 self.composite(canvas, tex, size, inv, BlendMode::Normal, 1.0, project.scaler, None)
1019 }
1020
1021 fn draw_clip(
1023 &mut self,
1024 project: &Project,
1025 clip: &Clip,
1026 t: f64,
1027 canvas: Target,
1028 layers: &LayerSet,
1029 extra: Extra,
1030 ) -> Target {
1031 let lt = clip.local(t);
1032 let opacity = clip.opacity.at(lt).clamp(0.0, 1.0) as f32 * extra.opacity * clip.fade_mult(lt) as f32;
1033 if opacity <= 0.0 || clip.kind == ClipKind::Audio {
1034 return canvas;
1035 }
1036 let (cw, ch) = (canvas.w, canvas.h);
1037 let s = cw as f32 / project.width.max(1) as f32;
1038
1039 if clip.kind == ClipKind::Adjustment {
1040 let Some(out) = self.run_chain(clip, t, project.fps, layers, canvas.tex, (cw, ch), s) else {
1042 return canvas;
1043 };
1044 self.pool.put(canvas);
1045 return out;
1046 }
1047
1048 let Some(frame) = layers.get(clip.id) else { return canvas };
1049 if frame.is_empty() {
1050 return canvas;
1051 }
1052 let contain = !matches!(clip.kind, ClipKind::Text | ClipKind::Shape);
1053 let native = self.native_size(project, clip, frame);
1054 let mut p = crate::engine::compose::placement(project, clip, t, native, cw, ch, contain);
1055 if !(p.w.is_finite() && p.h.is_finite() && p.w > 0.0 && p.h > 0.0) {
1056 return canvas;
1057 }
1058 let (mut focal, mut z0) = (cw as f32, 0.0f32);
1060 for e in clip.effects.iter().filter(|e| e.on_at(lt) && e.kind.is_geometric()) {
1061 match e.kind {
1062 EffectKind::Wobble => {
1063 let (dx, dy, roll, yaw, pitch) = crate::engine::effects::wobble(e, lt);
1064 p.cx += dx as f32 * s;
1065 p.cy += dy as f32 * s;
1066 p.rot += roll as f32;
1067 p.yaw += yaw as f32;
1068 p.pitch += pitch as f32;
1069 }
1070 EffectKind::Plane3d => {
1071 p.yaw += e.at(0, lt) as f32;
1072 p.pitch += e.at(1, lt) as f32;
1073 p.rot += e.at(2, lt) as f32;
1074 let fov = (e.at(4, lt) as f32).clamp(1.0, 179.0).to_radians();
1075 focal = (cw as f32 / 2.0) / (fov / 2.0).tan() * (e.at(3, lt) as f32 / 2.0).max(0.01);
1076 z0 = e.at(5, lt) as f32 * cw as f32;
1077 }
1078 _ => {}
1079 }
1080 }
1081 p.cx += extra.dx;
1082 p.cy += extra.dy;
1083 let Some(inv) = inverse_placement(&p, focal, z0) else { return canvas };
1084
1085 let tex = self.upload(clip.id, frame);
1086 let lsize = (frame.width, frame.height);
1087 let img_scale = if contain { s * (lsize.0 as f32 / p.w.max(1e-3)) } else { s };
1090 let processed = self.run_chain(clip, t, project.fps, layers, tex, lsize, img_scale);
1091 let (src, size, own) = match &processed {
1092 Some(target) => (target.tex, (target.w, target.h), true),
1093 None => (tex, lsize, false),
1094 };
1095 let mask = match (extra.mask, &clip.mask) {
1096 (Some(m), _) => Some(MaskTex::Borrowed(m)),
1097 (None, Some(m)) if m.enabled => self.render_mask(m, (cw, ch), lt, s, (p.cx, p.cy)).map(MaskTex::Owned),
1098 _ => None,
1099 };
1100 let mask_tex = mask.as_ref().map(|m| m.tex());
1101 let out = self.composite(canvas, src, size, inv, clip.blend, opacity, project.scaler, mask_tex);
1102 if let Some(MaskTex::Owned(t)) = mask {
1103 self.pool.put(t);
1104 }
1105 if own {
1106 if let Some(t) = processed {
1107 self.pool.put(t);
1108 }
1109 }
1110 out
1111 }
1112
1113 #[allow(clippy::too_many_arguments)]
1115 fn run_chain(
1116 &mut self,
1117 clip: &Clip,
1118 t: f64,
1119 fps: f64,
1120 layers: &LayerSet,
1121 src: glow::Texture,
1122 size: (u32, u32),
1123 scale: f32,
1124 ) -> Option<Target> {
1125 let lt = clip.local(t);
1126 if let Some(g) = clip.graph.as_ref().filter(|_| clip.uses_graph()) {
1127 return self.eval_graph_on(g, clip, t, fps, layers, Some(src), size, scale);
1128 }
1129 let mut cur: Option<Target> = None;
1130 for e in &clip.effects {
1131 if !e.on_at(lt) || e.kind.is_geometric() {
1132 continue;
1133 }
1134 let input = cur.as_ref().map(|t| t.tex).unwrap_or(src);
1135 let mask = e
1136 .mask
1137 .as_ref()
1138 .filter(|m| m.enabled)
1139 .and_then(|m| self.render_mask(m, size, lt, scale, (size.0 as f32 / 2.0, size.1 as f32 / 2.0)));
1140 let motion = e.kind.needs_motion().then(|| self.motion_texs(clip.id, layers));
1141 let blob = self.track_blob(e, clip.id, lt, layers);
1142 let next = self.run_effect(input, size, e, lt, scale, mask.as_ref().map(|m| m.tex), motion, blob);
1143 if let Some(m) = mask {
1144 self.pool.put(m);
1145 }
1146 if let Some(n) = next {
1147 if let Some(old) = cur.replace(n) {
1148 self.pool.put(old);
1149 }
1150 }
1151 }
1152 cur
1153 }
1154
1155 #[allow(clippy::too_many_arguments)]
1157 fn eval_graph_on(
1158 &mut self,
1159 graph: &NodeGraph,
1160 clip: &Clip,
1161 t: f64,
1162 fps: f64,
1163 layers: &LayerSet,
1164 input: Option<glow::Texture>,
1165 size: (u32, u32),
1166 scale: f32,
1167 ) -> Option<Target> {
1168 let lt = clip.local(t);
1169 let order = graph.eval_order();
1170 let vals = graph.eval_values(lt, fps);
1172 let mut done: Vec<(Id, Target)> = Vec::with_capacity(order.len());
1173 let out_id = graph.output()?;
1174 for id in order {
1175 let Some(node) = graph.node(id) else { continue };
1176 let port = |p: usize| graph.input_of(id, p);
1177 let tex_of = |done: &Vec<(Id, Target)>, nid: Option<Id>| -> Option<glow::Texture> {
1178 let nid = nid?;
1179 done.iter().find(|(i, _)| *i == nid).map(|(_, t)| t.tex)
1180 };
1181 let a = tex_of(&done, port(0));
1182 let b = tex_of(&done, port(1));
1183 let result = if !node.enabled && node.kind.inputs() > 0 {
1184 a.and_then(|tex| self.copy_to(tex, size))
1185 } else {
1186 match &node.kind {
1187 NodeKind::Input => match input {
1188 Some(tex) => self.copy_to(tex, size),
1189 None => self.transparent(size),
1190 },
1191 NodeKind::Color(c) => {
1192 let target = self.acquire(size.0, size.1);
1193 if let Some(target) = &target {
1194 let a = c[3] as f32 / 255.0;
1195 self.clear(target, [c[0] as f32 / 255.0, c[1] as f32 / 255.0, c[2] as f32 / 255.0, a]);
1196 }
1197 target
1198 }
1199 NodeKind::Clip(other) | NodeKind::Asset(other) => match layers.get(*other) {
1202 Some(f) if !f.is_empty() => {
1203 let tex = self.upload(*other, f);
1204 self.copy_to(tex, size)
1205 }
1206 _ => self.transparent(size),
1207 },
1208 NodeKind::String(style) => self.text_node(id, style, t, lt, fps, scale, size),
1209 NodeKind::Number(_)
1211 | NodeKind::Bool(_)
1212 | NodeKind::Random { .. }
1213 | NodeKind::Math(_)
1214 | NodeKind::Compare(_)
1215 | NodeKind::Logic(_) => {
1216 let v = vals.get(&id).copied().unwrap_or(0.0).clamp(0.0, 1.0) as f32;
1217 let target = self.acquire(size.0, size.1);
1218 if let Some(target) = &target {
1219 self.clear(target, [v, v, v, 1.0]);
1220 }
1221 target
1222 }
1223 NodeKind::Select => {
1224 let cond = graph.input_of(id, 0).and_then(|f| vals.get(&f).copied()).unwrap_or(0.0);
1225 match if cond >= 0.5 { b } else { tex_of(&done, port(2)) } {
1226 Some(tex) => self.copy_to(tex, size),
1227 None => self.transparent(size),
1228 }
1229 }
1230 NodeKind::Effect(_) if a.is_none() => self.transparent(size),
1231 NodeKind::Effect(e) => {
1232 let owned = driven(e, graph, id, &vals);
1233 let e = &*owned;
1234 let src = a.unwrap_or(self.blank);
1235 let mask = e.mask.as_ref().filter(|m| m.enabled).and_then(|m| {
1236 self.render_mask(m, size, lt, scale, (size.0 as f32 / 2.0, size.1 as f32 / 2.0))
1237 });
1238 let motion = e.kind.needs_motion().then(|| self.motion_texs(clip.id, layers));
1239 let blob = self.track_blob(e, clip.id, lt, layers);
1240 let r = self.run_effect(src, size, e, lt, scale, mask.as_ref().map(|m| m.tex), motion, blob);
1241 if let Some(m) = mask {
1242 self.pool.put(m);
1243 }
1244 match r {
1246 Some(target) => Some(target),
1247 None => self.copy_to(src, size),
1248 }
1249 }
1250 NodeKind::Blend { .. } | NodeKind::Combine { .. } | NodeKind::Merge => {
1252 let (mode, amount) = match &node.kind {
1253 NodeKind::Blend { mode, opacity } => (*mode, opacity.at(lt)),
1254 NodeKind::Combine { mode, factor } => (*mode, factor.at(lt)),
1255 _ => (BlendMode::Normal, 1.0),
1256 };
1257 let amount = graph.input_of(id, 2).and_then(|f| vals.get(&f).copied()).unwrap_or(amount);
1259 let under = match a {
1260 Some(tex) => self.copy_to(tex, size),
1261 None => self.transparent(size),
1262 };
1263 match (under, b) {
1264 (Some(under), Some(over)) => {
1265 let inv =
1266 [[1.0 / size.0 as f32, 0.0, 0.0], [0.0, 1.0 / size.1 as f32, 0.0], [0.0, 0.0, 1.0]];
1267 let op = amount.clamp(0.0, 1.0) as f32;
1268 Some(self.composite(under, over, size, inv, mode, op, Scaler::Bilinear, None))
1269 }
1270 (u, _) => u,
1271 }
1272 }
1273 NodeKind::Matte { invert, use_alpha } => match (a, b) {
1274 (Some(a), Some(b)) => self.matte(a, b, size, *invert, *use_alpha),
1275 (Some(a), None) => self.copy_to(a, size),
1276 _ => self.transparent(size),
1277 },
1278 NodeKind::Mask(m) => {
1279 self.render_mask(m, size, lt, scale, (size.0 as f32 / 2.0, size.1 as f32 / 2.0))
1280 }
1281 NodeKind::Output => match a {
1282 Some(tex) => self.copy_to(tex, size),
1283 None => self.transparent(size),
1284 },
1285 }
1286 };
1287 if let Some(r) = result {
1288 done.push((id, r));
1289 }
1290 }
1291 self.finish_graph(done, out_id)
1292 }
1293
1294 fn finish_graph(&mut self, mut done: Vec<(Id, Target)>, out_id: Id) -> Option<Target> {
1296 let idx = done.iter().position(|(i, _)| *i == out_id);
1297 let out = idx.map(|i| done.swap_remove(i).1);
1298 for (_, t) in done {
1299 self.pool.put(t);
1300 }
1301 out
1302 }
1303
1304 fn transparent(&mut self, size: (u32, u32)) -> Option<Target> {
1305 let t = self.acquire(size.0, size.1)?;
1306 self.clear(&t, [0.0; 4]);
1307 Some(t)
1308 }
1309
1310 #[allow(clippy::too_many_arguments)]
1313 fn text_node(
1314 &mut self,
1315 id: Id,
1316 style: &crate::model::TextStyle,
1317 t: f64,
1318 lt: f64,
1319 fps: f64,
1320 scale: f32,
1321 size: (u32, u32),
1322 ) -> Option<Target> {
1323 let rasterizer = self.text.clone()?;
1324 let mut style = style.clone();
1325 style.text = crate::model::expand_text(&style.text, t, lt, fps);
1326 let img = rasterizer.lock().ok()?.render(&style, scale);
1327 let base = self.transparent(size)?;
1328 if img.is_empty() || (img.width <= 1 && img.height <= 1) {
1329 return Some(base);
1330 }
1331 let p = crate::engine::compose::Placement {
1332 cx: size.0 as f32 / 2.0,
1333 cy: size.1 as f32 / 2.0,
1334 w: img.width as f32,
1335 h: img.height as f32,
1336 rot: 0.0,
1337 yaw: 0.0,
1338 pitch: 0.0,
1339 };
1340 let Some(inv) = inverse_placement(&p, size.0 as f32, 0.0) else { return Some(base) };
1341 let tex = self.upload(id, &img);
1342 let wh = (img.width, img.height);
1343 Some(self.composite(base, tex, wh, inv, BlendMode::Normal, 1.0, Scaler::Bilinear, None))
1344 }
1345
1346 fn matte(
1347 &mut self,
1348 a: glow::Texture,
1349 b: glow::Texture,
1350 size: (u32, u32),
1351 invert: bool,
1352 use_alpha: bool,
1353 ) -> Option<Target> {
1354 self.ensure(K_MATTE).ok()?;
1355 let dst = self.acquire(size.0, size.1)?;
1356 let gl = self.gl.clone();
1357 self.draw(K_MATTE, &dst, a, None, |p| unsafe {
1358 gl.uniform_2_f32(p.uni.get("u_res"), size.0 as f32, size.1 as f32);
1359 gl.active_texture(glow::TEXTURE0 + U_B);
1360 gl.bind_texture(glow::TEXTURE_2D, Some(b));
1361 gl.uniform_1_i32(p.uni.get("u_b"), U_B as i32);
1362 gl.uniform_1_i32(p.uni.get("u_matte_alpha"), i32::from(use_alpha));
1363 gl.uniform_1_i32(p.uni.get("u_matte_invert"), i32::from(invert));
1364 });
1365 Some(dst)
1366 }
1367
1368 fn native_size(&self, project: &Project, clip: &Clip, frame: &Frame) -> (u32, u32) {
1371 let wh = match clip.kind {
1372 ClipKind::Video | ClipKind::Image => project.asset(clip.asset).map(|a| (a.width, a.height)),
1373 ClipKind::Sequence => project.sequence(clip.sequence).map(|s| (s.width, s.height)),
1374 _ => None,
1375 };
1376 match wh {
1377 Some((w, h)) if w > 0 && h > 0 => (w, h),
1378 _ => (frame.width.max(1), frame.height.max(1)),
1379 }
1380 }
1381}
1382
1383impl Drop for GpuRenderer {
1384 fn drop(&mut self) {
1385 let gl = self.gl.clone();
1386 unsafe {
1387 for (_, t) in self.textures.drain() {
1388 gl.delete_texture(t.tex);
1389 }
1390 for t in self.loaned.drain(..).chain(self.pool.free.drain(..)) {
1391 gl.delete_framebuffer(t.fbo);
1392 gl.delete_texture(t.tex);
1393 }
1394 for (_, p) in self.programs.map.drain() {
1395 gl.delete_program(p.p);
1396 }
1397 gl.delete_shader(self.vert);
1398 gl.delete_texture(self.blank);
1399 gl.delete_vertex_array(self.vao);
1400 }
1401 }
1402}
1403
1404enum MaskTex {
1406 Owned(Target),
1407 Borrowed(glow::Texture),
1408}
1409
1410impl MaskTex {
1411 fn tex(&self) -> glow::Texture {
1412 match self {
1413 MaskTex::Owned(t) => t.tex,
1414 MaskTex::Borrowed(t) => *t,
1415 }
1416 }
1417}
1418
1419#[derive(Clone, Copy)]
1421struct Extra {
1422 opacity: f32,
1423 dx: f32,
1424 dy: f32,
1425 mask: Option<glow::Texture>,
1426}
1427
1428impl Extra {
1429 const NONE: Extra = Extra { opacity: 1.0, dx: 0.0, dy: 0.0, mask: None };
1430}
1431
1432const PARAM_NAMES: [&str; 12] = ["p0", "p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8", "p9", "p10", "p11"];
1434
1435pub const MAX_MASK_POINTS: usize = 64;
1437
1438fn param_values(effect: &Effect, t: f64) -> Vec<f64> {
1440 (0..effect.kind.params().len()).map(|i| effect.at(i, t)).collect()
1441}
1442
1443#[derive(Default)]
1446pub struct LayerSet {
1447 pub layers: Vec<(crate::model::Id, Arc<Frame>)>,
1448 pub motion: Vec<(crate::model::Id, f64, Arc<Frame>)>,
1452}
1453
1454impl LayerSet {
1455 pub const SUBTITLES: crate::model::Id = 0;
1457
1458 pub fn get(&self, id: crate::model::Id) -> Option<&Arc<Frame>> {
1459 self.layers.iter().find(|(i, _)| *i == id).map(|(_, f)| f)
1460 }
1461}
1462
1463pub fn driven<'a>(e: &'a Effect, graph: &NodeGraph, id: Id, vals: &HashMap<Id, f64>) -> std::borrow::Cow<'a, Effect> {
1468 let mut out = std::borrow::Cow::Borrowed(e);
1469 for i in 0..e.specs().len() {
1470 let Some(v) = graph.input_of(id, i + 1).and_then(|f| vals.get(&f).copied()) else { continue };
1471 let e = out.to_mut();
1472 while e.params.len() <= i {
1473 e.params.push(crate::model::Animated::new(0.0));
1474 }
1475 e.params[i] = crate::model::Animated::new(v);
1476 }
1477 out
1478}
1479
1480pub fn render_size(w: u32, h: u32, quality: f32) -> (u32, u32) {
1483 let q = quality.clamp(0.1, 1.0);
1484 (((w as f32 * q).round() as u32).max(1), ((h as f32 * q).round() as u32).max(1))
1485}
1486
1487pub fn blend_index(mode: BlendMode) -> i32 {
1490 BlendMode::ALL.iter().position(|m| *m == mode).unwrap_or(0) as i32
1491}
1492
1493pub fn scaler_index(s: Scaler) -> i32 {
1494 match s {
1495 Scaler::Nearest => 0,
1496 Scaler::Bilinear => 1,
1497 Scaler::Bicubic => 2,
1498 }
1499}
1500
1501pub fn placement_quad(p: &crate::engine::compose::Placement, f: f32, z0: f32) -> Option<[[f32; 2]; 4]> {
1505 let (hw, hh) = (p.w / 2.0, p.h / 2.0);
1506 let (sy, cy) = p.yaw.to_radians().sin_cos();
1507 let (sp, cp) = p.pitch.to_radians().sin_cos();
1508 let (sr, cr) = p.rot.to_radians().sin_cos();
1509 let f = if f.is_finite() && f > 1.0 { f } else { 1.0 };
1510 let mut quad = [[0.0f32; 2]; 4];
1511 for (k, (x, y)) in [(-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)].into_iter().enumerate() {
1512 let x1 = x * cy;
1513 let z1 = -x * sy;
1514 let y2 = y * cp - z1 * sp;
1515 let z2 = y * sp + z1 * cp + z0;
1516 let d = f + z2;
1517 if d < f * 0.05 {
1518 return None; }
1520 let px = f * x1 / d;
1521 let py = f * y2 / d;
1522 quad[k] = [p.cx + px * cr - py * sr, p.cy + px * sr + py * cr];
1523 }
1524 Some(quad)
1525}
1526
1527pub fn square_to_quad(q: &[[f32; 2]; 4]) -> [[f32; 3]; 3] {
1531 let [p0, p1, p2, p3] = *q;
1532 let (dx1, dy1) = (p1[0] - p2[0], p1[1] - p2[1]);
1533 let (dx2, dy2) = (p3[0] - p2[0], p3[1] - p2[1]);
1534 let (sx, sy) = (p0[0] - p1[0] + p2[0] - p3[0], p0[1] - p1[1] + p2[1] - p3[1]);
1535 let (g, h) = if sx.abs() < 1e-6 && sy.abs() < 1e-6 {
1536 (0.0, 0.0)
1537 } else {
1538 let den = dx1 * dy2 - dx2 * dy1;
1539 if den.abs() < 1e-9 {
1540 (0.0, 0.0)
1541 } else {
1542 ((sx * dy2 - dx2 * sy) / den, (dx1 * sy - sx * dy1) / den)
1543 }
1544 };
1545 [
1546 [p1[0] - p0[0] + g * p1[0], p3[0] - p0[0] + h * p3[0], p0[0]],
1547 [p1[1] - p0[1] + g * p1[1], p3[1] - p0[1] + h * p3[1], p0[1]],
1548 [g, h, 1.0],
1549 ]
1550}
1551
1552pub fn invert3(m: &[[f32; 3]; 3]) -> Option<[[f32; 3]; 3]> {
1554 let [a, b, c] = [m[0][0] as f64, m[0][1] as f64, m[0][2] as f64];
1555 let [d, e, f] = [m[1][0] as f64, m[1][1] as f64, m[1][2] as f64];
1556 let [g, h, i] = [m[2][0] as f64, m[2][1] as f64, m[2][2] as f64];
1557 let det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
1558 if det.abs() < 1e-12 {
1559 return None;
1560 }
1561 let k = 1.0 / det;
1562 Some([
1563 [((e * i - f * h) * k) as f32, ((c * h - b * i) * k) as f32, ((b * f - c * e) * k) as f32],
1564 [((f * g - d * i) * k) as f32, ((a * i - c * g) * k) as f32, ((c * d - a * f) * k) as f32],
1565 [((d * h - e * g) * k) as f32, ((b * g - a * h) * k) as f32, ((a * e - b * d) * k) as f32],
1566 ])
1567}
1568
1569pub fn inverse_placement(p: &crate::engine::compose::Placement, f: f32, z0: f32) -> Option<[[f32; 3]; 3]> {
1571 invert3(&square_to_quad(&placement_quad(p, f, z0)?))
1572}
1573
1574fn column_major(m: &[[f32; 3]; 3]) -> [f32; 9] {
1576 [m[0][0], m[1][0], m[2][0], m[0][1], m[1][1], m[2][1], m[0][2], m[1][2], m[2][2]]
1577}
1578
1579fn user_key(src: &str) -> u64 {
1582 USER_BIT | hash_str(src)
1583}
1584
1585fn hash_str(s: &str) -> u64 {
1586 use std::hash::{Hash, Hasher};
1587 let mut h = std::collections::hash_map::DefaultHasher::new();
1588 s.hash(&mut h);
1589 h.finish() & !USER_BIT
1590}
1591
1592unsafe fn tex_params(gl: &glow::Context) {
1593 gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR as i32);
1596 gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32);
1597 gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32);
1598 gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32);
1599}
1600
1601unsafe fn compile(gl: &glow::Context, kind: u32, src: &str) -> Result<glow::Shader, String> {
1602 let sh = gl.create_shader(kind).map_err(|e| format!("create_shader: {e}"))?;
1603 gl.shader_source(sh, src);
1604 gl.compile_shader(sh);
1605 if !gl.get_shader_compile_status(sh) {
1606 let log = gl.get_shader_info_log(sh);
1607 gl.delete_shader(sh);
1608 return Err(log);
1609 }
1610 Ok(sh)
1611}
1612
1613unsafe fn build(gl: &glow::Context, vert: glow::Shader, frag_src: &str) -> Result<Prog, String> {
1614 let frag = compile(gl, glow::FRAGMENT_SHADER, frag_src)?;
1615 let p = gl.create_program().map_err(|e| format!("create_program: {e}"))?;
1616 gl.attach_shader(p, vert);
1617 gl.attach_shader(p, frag);
1618 gl.link_program(p);
1619 gl.detach_shader(p, vert);
1620 gl.detach_shader(p, frag);
1621 gl.delete_shader(frag);
1622 if !gl.get_program_link_status(p) {
1623 let log = gl.get_program_info_log(p);
1624 gl.delete_program(p);
1625 return Err(log);
1626 }
1627 let mut uni = HashMap::new();
1628 for name in UNIFORMS {
1629 if let Some(l) = gl.get_uniform_location(p, name) {
1630 uni.insert(*name, l);
1631 }
1632 }
1633 if let Some(l) = gl.get_uniform_location(p, "m_points[0]") {
1635 uni.entry("m_points").or_insert(l);
1636 }
1637 Ok(Prog { p, uni })
1638}
1639
1640#[cfg(test)]
1641mod tests {
1642 use super::*;
1643 use crate::engine::compose::Placement;
1644 use std::num::NonZeroU32;
1645
1646 #[test]
1647 fn a_value_node_drives_an_effect_parameter() {
1648 let mut next = 0;
1649 let mut nid = || {
1650 next += 1;
1651 next
1652 };
1653 let mut g = NodeGraph::new(&mut nid);
1654 let fx_id = nid();
1655 let num_id = nid();
1656 g.nodes.push(crate::model::Node {
1657 id: fx_id,
1658 kind: NodeKind::Effect(Effect::new(EffectKind::Blur)),
1659 x: 0.0,
1660 y: 0.0,
1661 enabled: true,
1662 });
1663 g.nodes.push(crate::model::Node {
1664 id: num_id,
1665 kind: NodeKind::Number(crate::model::Animated::new(42.0)),
1666 x: 0.0,
1667 y: 0.0,
1668 enabled: true,
1669 });
1670 let out = g.output().unwrap();
1671 assert!(g.connect(fx_id, out, 0));
1672 let fx = Effect::new(EffectKind::Blur);
1673 let vals = g.eval_values(0.0, 30.0);
1675 assert!(matches!(driven(&fx, &g, fx_id, &vals), std::borrow::Cow::Borrowed(_)));
1676 assert!(g.connect(num_id, fx_id, 1));
1678 let vals = g.eval_values(0.0, 30.0);
1679 assert_eq!(driven(&fx, &g, fx_id, &vals).at(0, 0.0), 42.0);
1680 assert_eq!(fx.at(0, 0.0), Effect::new(EffectKind::Blur).at(0, 0.0), "the stored effect is untouched");
1681 }
1682
1683 fn fake_target(w: u32, h: u32, id: u32) -> Target {
1684 Target {
1686 tex: glow::NativeTexture(NonZeroU32::new(id).unwrap()),
1687 fbo: glow::NativeFramebuffer(NonZeroU32::new(id).unwrap()),
1688 w,
1689 h,
1690 }
1691 }
1692
1693 #[test]
1694 fn pool_reuses_only_matching_sizes() {
1695 let mut p = Pool::default();
1696 assert!(p.take(16, 16).is_none());
1697 p.created();
1698 assert_eq!(p.live, 1);
1699 p.put(fake_target(16, 16, 1));
1700 assert_eq!(p.live, 0);
1701 assert_eq!(p.free.len(), 1);
1702 assert!(p.take(32, 16).is_none());
1704 assert_eq!(p.free.len(), 1);
1705 let t = p.take(16, 16).expect("reuse");
1706 assert_eq!((t.w, t.h), (16, 16));
1707 assert_eq!(p.live, 1);
1708 assert!(p.free.is_empty());
1709 p.put(t);
1710 p.put(fake_target(8, 8, 2));
1711 assert_eq!(p.free.len(), 2);
1712 assert_eq!(p.live, 0);
1714 }
1715
1716 #[test]
1717 fn pool_evicts_the_oldest_when_the_free_list_is_over_budget() {
1718 let mut p = Pool::default();
1719 let side = (Pool::MAX_FREE_PX as f64).sqrt() as u32;
1721 p.put(fake_target(side, side, 1));
1722 p.put(fake_target(64, 64, 2));
1723 assert_eq!(p.free.len(), 1, "the oversized free list must shrink");
1724 assert_eq!((p.free[0].w, p.free[0].h), (64, 64), "the newest target survives");
1725 let mut p = Pool::default();
1727 for i in 0..8 {
1728 p.put(fake_target(1920, 1080, i + 1));
1729 }
1730 assert_eq!(p.free.len(), 8, "8 × 1080p is well inside the budget");
1731 }
1732
1733 #[test]
1734 fn every_effect_parameter_reaches_a_uniform() {
1735 let most = crate::model::EffectKind::ALL.iter().map(|k| k.params().len()).max().unwrap_or(0);
1736 assert!(most <= PARAM_NAMES.len(), "{most} params but only {} are uploaded", PARAM_NAMES.len());
1737 for n in PARAM_NAMES {
1738 assert!(UNIFORMS.contains(&n), "{n} is never located at link time");
1739 }
1740 }
1741
1742 #[test]
1743 fn blend_int_mapping_matches_engine_blend() {
1744 assert_eq!(blend_index(BlendMode::Normal), 0);
1745 for (i, m) in BlendMode::ALL.iter().enumerate() {
1746 assert_eq!(blend_index(*m), i as i32, "{}", m.name());
1747 }
1748 let mut seen = Vec::new();
1750 for line in shaders::BLEND.lines() {
1751 let Some(rest) = line.split("mode == ").nth(1) else { continue };
1752 let Some(n) = rest.split(')').next().and_then(|s| s.trim().parse::<usize>().ok()) else {
1753 continue;
1754 };
1755 let Some(name) = line.split("// ").nth(1) else { continue };
1756 seen.push((n, name.trim().to_string()));
1757 }
1758 seen.dedup();
1759 assert_eq!(seen.len(), 12, "expected one branch per non-Normal mode: {seen:?}");
1760 for (n, name) in seen {
1761 let want = BlendMode::ALL[n].name().replace(' ', "");
1762 assert_eq!(name, want, "GLSL branch {n} is {name}, enum says {want}");
1763 }
1764 }
1765
1766 #[test]
1767 fn render_size_scales_and_never_collapses() {
1768 assert_eq!(render_size(1920, 1080, 1.0), (1920, 1080));
1769 assert_eq!(render_size(1920, 1080, 0.5), (960, 540));
1770 assert_eq!(render_size(1, 1, 0.0), (1, 1), "quality is clamped, never zero-sized");
1771 assert_eq!(render_size(640, 480, 2.0), (640, 480), "never upscales past 1.0");
1772 }
1773
1774 #[test]
1775 fn scaler_mapping() {
1776 assert_eq!(scaler_index(Scaler::Nearest), 0);
1777 assert_eq!(scaler_index(Scaler::Bilinear), 1);
1778 assert_eq!(scaler_index(Scaler::Bicubic), 2);
1779 }
1780
1781 fn map(m: &[[f32; 3]; 3], x: f32, y: f32) -> (f32, f32) {
1782 let w = m[2][0] * x + m[2][1] * y + m[2][2];
1783 ((m[0][0] * x + m[0][1] * y + m[0][2]) / w, (m[1][0] * x + m[1][1] * y + m[1][2]) / w)
1784 }
1785
1786 #[test]
1787 fn flat_quad_matches_placement_bounds() {
1788 let p = Placement { cx: 100.0, cy: 60.0, w: 80.0, h: 40.0, rot: 0.0, yaw: 0.0, pitch: 0.0 };
1789 let q = placement_quad(&p, 640.0, 0.0).unwrap();
1790 let (x0, y0, x1, y1) = p.bounds();
1791 assert_eq!(q, [[x0, y0], [x1, y0], [x1, y1], [x0, y1]]);
1792 let r = Placement { rot: 30.0, ..p };
1794 let q = placement_quad(&r, 640.0, 0.0).unwrap();
1795 let (bx0, by0, bx1, by1) = r.bounds();
1796 let xs: Vec<f32> = q.iter().map(|c| c[0]).collect();
1797 let ys: Vec<f32> = q.iter().map(|c| c[1]).collect();
1798 let mn = |v: &[f32]| v.iter().cloned().fold(f32::MAX, f32::min);
1799 let mx = |v: &[f32]| v.iter().cloned().fold(f32::MIN, f32::max);
1800 for (a, b) in [(mn(&xs), bx0), (mx(&xs), bx1), (mn(&ys), by0), (mx(&ys), by1)] {
1801 assert!((a - b).abs() < 1e-3, "{a} vs {b}");
1802 }
1803 }
1804
1805 #[test]
1806 fn yaw_zero_is_the_affine_inverse() {
1807 let p = Placement { cx: 160.0, cy: 90.0, w: 320.0, h: 180.0, rot: 0.0, yaw: 0.0, pitch: 0.0 };
1808 let inv = inverse_placement(&p, 320.0, 0.0).unwrap();
1809 for ((x, y), (u, v)) in [
1811 ((0.0f32, 0.0f32), (0.0f32, 0.0f32)),
1812 ((320.0, 0.0), (1.0, 0.0)),
1813 ((320.0, 180.0), (1.0, 1.0)),
1814 ((160.0, 90.0), (0.5, 0.5)),
1815 ] {
1816 let (mu, mv) = map(&inv, x, y);
1817 assert!((mu - u).abs() < 1e-4 && (mv - v).abs() < 1e-4, "({x},{y}) -> ({mu},{mv})");
1818 }
1819 assert!(inv[2][0].abs() < 1e-6 && inv[2][1].abs() < 1e-6);
1821 }
1822
1823 #[test]
1824 fn yaw_tilts_and_stays_invertible() {
1825 let p = Placement { cx: 160.0, cy: 90.0, w: 200.0, h: 100.0, rot: 0.0, yaw: 40.0, pitch: 0.0 };
1826 let q = placement_quad(&p, 320.0, 0.0).unwrap();
1827 let left_h = (q[3][1] - q[0][1]).abs();
1829 let right_h = (q[2][1] - q[1][1]).abs();
1830 assert!(right_h > left_h, "{right_h} !> {left_h}");
1831 let q2 = placement_quad(&Placement { yaw: -40.0, ..p }, 320.0, 0.0).unwrap();
1833 assert!((q2[2][1] - q2[1][1]).abs() < (q2[3][1] - q2[0][1]).abs());
1834 let inv = inverse_placement(&p, 320.0, 0.0).unwrap();
1835 let (u, v) = map(&inv, p.cx, p.cy);
1836 assert!((0.0..=1.0).contains(&u) && (v - 0.5).abs() < 1e-3, "centre -> ({u},{v})");
1837 let bad = Placement { w: 4000.0, yaw: 89.0, ..p };
1839 assert!(placement_quad(&bad, 320.0, 0.0).is_none());
1840 }
1841
1842 #[test]
1843 fn matrix_uploads_column_major() {
1844 let m = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
1845 assert_eq!(column_major(&m), [1.0, 4.0, 7.0, 2.0, 5.0, 8.0, 3.0, 6.0, 9.0]);
1846 }
1847
1848 #[test]
1849 fn user_shader_keys_differ_per_source() {
1850 assert_ne!(hash_str("a"), hash_str("b"));
1851 assert_eq!(hash_str("a"), hash_str("a"));
1852 assert_eq!(hash_str("a") & USER_BIT, 0, "the user bit must be free for tagging");
1853 assert!((USER_BIT | hash_str("a")) > EFFECT_BASE + 64);
1854 assert_ne!(user_key("a"), user_key("b"));
1857 assert_eq!(user_key("a"), USER_BIT | hash_str("a"));
1858 }
1859
1860 #[test]
1864 fn every_program_source_has_exactly_one_entry_point() {
1865 let mut sources = vec![
1866 format!("{}{}{}", shaders::PRELUDE, shaders::BLEND, shaders::COMPOSITE),
1867 format!("{}{}", shaders::PRELUDE, shaders::MASK),
1868 format!(
1869 "{}
1870{}
1871{}",
1872 shaders::PRELUDE,
1873 COPY_BODY,
1874 shaders::MAIN
1875 ),
1876 format!(
1877 "{}
1878{}
1879{}",
1880 shaders::PRELUDE,
1881 MATTE_BODY,
1882 shaders::MAIN
1883 ),
1884 ];
1885 sources.extend(
1886 crate::model::EffectKind::ALL.iter().filter_map(|k| shaders::fragment(*k, crate::model::DEFAULT_SHADER)),
1887 );
1888 for src in &sources {
1889 let n = src.matches("void main()").count();
1890 assert_eq!(n, 1, "{n} entry points in {}", &src[..80.min(src.len())]);
1891 assert!(src.starts_with("#version 330 core"), "{}", &src[..40.min(src.len())]);
1892 assert_eq!(
1894 src.contains("effect(src, uv)"),
1895 src.contains("vec4 effect(vec4 src, vec2 uv)"),
1896 "{}",
1897 &src[..80.min(src.len())]
1898 );
1899 }
1900 }
1901}