diff --git a/crates/engine/src/tests/proxy.rs b/crates/engine/src/tests/proxy.rs
--- a/crates/engine/src/tests/proxy.rs
+++ b/crates/engine/src/tests/proxy.rs
@@ -14,7 +14,8 @@ use crate::Session;
 use crate::render::{FootageSource, RenderOpts};
 
 /// Files by name: `red16.png` (16×16 red), `blue8.png` (8×8 blue), `lines.mov` (interlaced:
-/// even rows red, odd rows blue; 4×4, 10 fps), `matte_black.png` / `matte_white.png` (a
+/// even rows red, odd rows blue; 4×4, 10 fps), `flash.mov` (10×10, 10 fps: red, then blue from
+/// the second frame), `matte_black.png` / `matte_white.png` (a
 /// semi-transparent green premultiplied against black / white, read as straight), `half.png`
 /// (white at 25 % alpha), `wide.png` (10×10 white).
 struct Synthetic;
@@ -52,9 +53,14 @@ fn size_of(path: &str) -> (u32, u32) {
 }
 
 impl FootageSource for Synthetic {
-    fn frame(&self, _: ItemId, f: &Footage, _t: Tick) -> Option<Arc<Image>> {
+    fn frame(&self, _: ItemId, f: &Footage, t: Tick) -> Option<Arc<Image>> {
         let (w, h) = size_of(&f.path);
         let mut img = Image::new(w, h);
+        // `flash.mov` is red for its first frame and blue from its second (0.1 s) on.
+        if f.path.ends_with("flash.mov") {
+            let c = if t.seconds() >= 0.1 - 1e-9 { [0.0, 0.0, 1.0, 1.0] } else { [1.0, 0.0, 0.0, 1.0] };
+            return Some(Arc::new(Image::filled(w, h, c)));
+        }
         for y in 0..h {
             for x in 0..w {
                 // Decoders hand out premultiplied pixels (straight file colour × alpha).
@@ -171,6 +177,118 @@ fn comp_proxy_and_create_proxy() {
     assert!(blue(px(&s, outer, 0.0, o, 8, 8)));
 }
 
+/// Outer (4×4, 20 fps) holding a layer of Inner (4×4, 10 fps, a red solid): (inner, outer).
+fn nested(s: &mut Session) -> (ItemId, ItemId) {
+    s.execute("comp.new", json!({"name": "Inner", "width": 4, "height": 4, "frameRate": 10, "duration": 2})).unwrap();
+    let inner = s.active_comp_id().unwrap();
+    s.execute("layer.newSolid", json!({"color": "#ff0000"})).unwrap();
+    s.execute("comp.new", json!({"name": "Outer", "width": 4, "height": 4, "frameRate": 20, "duration": 2})).unwrap();
+    let outer = s.active_comp_id().unwrap();
+    s.execute("layer.addItem", json!({"item": inner.0})).unwrap();
+    (inner, outer)
+}
+
+/// Interpret Footage ▸ Proxy's Invert Alpha and Interpret As Linear Light change what a
+/// composition proxy draws, as they do for the same file used as footage.
+#[test]
+fn comp_proxy_draws_with_its_interpretation() {
+    let mut s = session();
+    let (inner, outer) = nested(&mut s);
+    let o = RenderOpts::default();
+    // Invert Alpha: the 25 % proxy covers 75 %.
+    s.execute("file.setProxy", json!({"item": inner.0, "path": "/m/half.png"})).unwrap();
+    s.execute("file.interpretProxy", json!({"item": inner.0, "alpha": "straight", "invertAlpha": true})).unwrap();
+    let p = px(&s, outer, 0.0, o, 1, 1);
+    assert!((p[3] - 0.75).abs() < 0.01, "{p:?}");
+    // Interpret As Linear Light: the proxy draws as the file does as footage with that setting.
+    let f = import(&mut s, "/m/matte_black.png");
+    s.execute("file.interpretFootage", json!({"item": f.0, "linearLight": true})).unwrap();
+    s.execute("comp.new", json!({"name": "Ref", "width": 4, "height": 4, "frameRate": 20, "duration": 2})).unwrap();
+    let r = s.active_comp_id().unwrap();
+    s.execute("layer.addItem", json!({"item": f.0})).unwrap();
+    let want = px(&s, r, 0.0, o, 1, 1);
+    assert!(want[1] / want[3] > 0.7, "linear light re-encodes the 0.5 green: {want:?}");
+    s.execute("file.setProxy", json!({"item": inner.0, "path": "/m/matte_black.png"})).unwrap();
+    s.execute("file.interpretProxy", json!({"item": inner.0, "linearLight": true})).unwrap();
+    let p = px(&s, outer, 0.0, o, 1, 1);
+    assert!((0..4).all(|i| (p[i] - want[i]).abs() < 1e-3), "{p:?} != {want:?}");
+}
+
+/// Interpret Footage ▸ Proxy ▸ Separate Fields: a composition proxy shows one field per frame
+/// at twice the rate, as footage does.
+#[test]
+fn comp_proxy_separates_fields() {
+    let mut s = session();
+    let (inner, outer) = nested(&mut s);
+    let o = RenderOpts::default();
+    s.execute("file.setProxy", json!({"item": inner.0, "path": "/m/lines.mov"})).unwrap();
+    assert!(red(px(&s, outer, 0.05, o, 1, 0)) && blue(px(&s, outer, 0.05, o, 1, 1)), "progressive");
+    s.execute("file.interpretProxy", json!({"item": inner.0, "fields": "upper"})).unwrap();
+    // At 0.05 s the lower field (odd rows, blue) is shown, with the even rows interpolated.
+    for y in 0..3 {
+        assert!(blue(px(&s, outer, 0.05, o, 1, y)), "lower field at row {y}");
+    }
+}
+
+/// Frame Blending on a precomp layer blends the frames of the composition's proxy.
+#[test]
+fn comp_proxy_frames_blend() {
+    let mut s = session();
+    let (inner, outer) = nested(&mut s);
+    let o = RenderOpts::default();
+    s.execute("file.setProxy", json!({"item": inner.0, "path": "/m/flash.mov"})).unwrap();
+    assert!(red(px(&s, outer, 0.05, o, 1, 1)));
+    s.execute("layer.frameBlending", json!({"mode": "frameMix"})).unwrap();
+    s.execute("comp.setSwitch", json!({"switch": "frameBlending", "value": true})).unwrap();
+    // Halfway between the red first frame and the blue second one.
+    let p = px(&s, outer, 0.05, o, 1, 1);
+    assert!((p[0] - 0.5).abs() < 0.05 && (p[2] - 0.5).abs() < 0.05 && p[3] > 0.99, "{p:?}");
+    assert!(blue(px(&s, outer, 0.1, o, 1, 1)));
+}
+
+#[test]
+fn comp_proxy_frame_blend_cache_tracks_alpha_interpretation() {
+    struct Interpreted(std::sync::atomic::AtomicUsize);
+    impl FootageSource for Interpreted {
+        fn frame(&self, _: ItemId, f: &Footage, _: Tick) -> Option<Arc<Image>> {
+            self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+            let pixel = match f.alpha {
+                AlphaMode::Straight => [0.4, 0.2, 0.1, 0.5],
+                AlphaMode::Premultiplied => [f.premul_color[0] * 0.5, 0.1, 0.0, 0.5],
+                AlphaMode::Ignore => [0.8, 0.4, 0.2, 1.0],
+            };
+            Some(Arc::new(Image::filled(4, 4, pixel)))
+        }
+    }
+    for mode in ["frameMix", "pixelMotion"] {
+        let mut s = session();
+        let footage = Arc::new(Interpreted(std::sync::atomic::AtomicUsize::new(0)));
+        s.footage = footage.clone();
+        let (inner, outer) = nested(&mut s);
+        s.execute("file.setProxy", json!({"item": inner.0, "path": "/m/alpha.mov"})).unwrap();
+        s.execute("file.interpretProxy", json!({"item": inner.0, "alpha": "straight"})).unwrap();
+        s.execute("layer.frameBlending", json!({"mode": mode})).unwrap();
+        s.execute("comp.setSwitch", json!({"switch": "frameBlending", "value": true})).unwrap();
+        let opts = RenderOpts::default();
+        let first = px(&s, outer, 0.05, opts, 1, 1);
+        let calls = footage.0.load(std::sync::atomic::Ordering::Relaxed);
+        assert_eq!(px(&s, outer, 0.05, opts, 1, 1), first);
+        assert_eq!(footage.0.load(std::sync::atomic::Ordering::Relaxed), calls);
+        for params in [
+            json!({"item": inner.0, "alpha": "ignore"}),
+            json!({"item": inner.0, "alpha": "premultiplied", "premulColor": [0.0, 0.0, 0.0]}),
+            json!({"item": inner.0, "alpha": "premultiplied", "premulColor": [1.0, 0.0, 0.0]}),
+        ] {
+            s.execute("file.interpretProxy", params.clone()).unwrap();
+            let mut fresh = s.renderer(opts);
+            fresh.cache = None;
+            let want = fresh.comp_frame(outer, Tick::from_seconds_f64(0.05)).get(1, 1);
+            let actual = px(&s, outer, 0.05, opts, 1, 1);
+            assert_eq!(actual, want, "{mode} after {params}");
+        }
+    }
+}
+
 #[test]
 fn interpret_proxy_settings_apply_to_the_proxy() {
     let mut s = session();
diff --git a/crates/render/src/pipeline.rs b/crates/render/src/pipeline.rs
--- a/crates/render/src/pipeline.rs
+++ b/crates/render/src/pipeline.rs
@@ -595,8 +595,10 @@ impl<'a> Renderer<'a> {
                 if self.project.comp_contains(*item, ctx.comp_id) {
                     return None;
                 }
-                // A composition proxy (a rendered still or movie) stands in for the comp.
-                // Nothing before the nested comp starts or after it ends.
+                // A composition proxy (a rendered still or movie) stands in for the comp. It is
+                // decoded like footage: its Separate Fields, Invert Alpha and Interpret As Linear
+                // Light settings and the layer's Frame Blending apply. Nothing before the nested
+                // comp starts or after it ends.
                 let lt = ctx.nested_time(layer);
                 if !self.project.comp(*item).is_some_and(|nc| nc.covers(lt)) {
                     return None;
@@ -604,7 +606,7 @@ impl<'a> Renderer<'a> {
                 if let Some(pf) = self.proxy_for(*item)
                     && let Some(nc) = self.project.comp(*item)
                 {
-                    let img = self.footage.frame_with(*item, pf, lt, None, self.pipe.float_encoding())?;
+                    let img = self.footage_frame(ctx, layer, *item, pf, lt, None)?;
                     return Some(self.footage_buf(&img, pf, nc.width, nc.height));
                 }
                 // Essential Properties overrides render the nested comp with this instance's
@@ -738,6 +740,10 @@ impl<'a> Renderer<'a> {
                     use std::hash::{Hash, Hasher};
                     let mut h = std::collections::hash_map::DefaultHasher::new();
                     (0x00f8_a3e5_u64, item, i, (w as f32).to_bits(), mode as u8, f.frame_rate.num, f.frame_rate.den, &f.path, float).hash(&mut h);
+                    (f.alpha as u8).hash(&mut h);
+                    if f.alpha == effectcraft_project::AlphaMode::Premultiplied {
+                        f.premul_color.map(f32::to_bits).hash(&mut h);
+                    }
                     h.finish()
                 });
                 if let (Some(c), Some(k)) = (self.cache, key)
