fix(app): probed stream indices for original media, central modal-defer, one less onscreen copy

- preview_footage_media decodes the original from the footage's actual
  first stream of the kind instead of hardcoded 0/1, fixing audio-first
  and other atypical stream layouts (with a unit test).
- spawn_modal now probes for a nested window update and defers the
  build instead of silently dropping the dialog — the phase-7
  Preferences/Action Search fix applied centrally to every modal
  (export, proxy settings, project manager, progress dialogs).
- The gpui_wgpu atlas no longer double-copies identity-format uploads,
  leaving a single CPU staging copy (gpui RenderImage ownership) plus
  the GPU upload on the onscreen path; the residual copy and the
  IOSurface route to true zero-copy are documented in the M15 design.
This commit is contained in:
2026-08-19 01:19:52 +08:00
parent adf2cef32c
commit 6dc8285f2a
4 changed files with 67 additions and 2 deletions
@@ -3,6 +3,7 @@
> 状态:已批准(用户 2026-08-18 提出,作为独立追加任务,不阻塞 M12 其余阶段)。
> 前置调研:见会话调研报告(oak-worker/ipc.rs 传输层已完整、TicketArena 投递口收敛、上屏链路 6 处拷贝点)。
> 进度:S1 完成(2026-08,协议 v2 + ProcessDispatcher + PreviewScheduler + worker 真实渲染,与线程池并存);S2 完成(默认 Processes、删除 WorkerPool、oaktask/oak-cli/app 接入、上屏零拷贝、播放预渲染窗口);S3 完成(2026-08-19:音频票走共享内存 + 播放异步预取 + 崩溃隔离覆盖音频、按票槽格式 F32 + 段按需扩容、worker/槽/批自适应策略 + 基准 bench_process)。
> 上屏拷贝现状(2026-08-19 修正):预览链路为 **1 次 CPU staging 拷贝 + GPU 上传**——shm 槽 → `RenderImage`gpui 的精灵图集要求 owned 缓冲与稳定 ImageId,借用型零拷贝需改造 vendored gpui 的 `RenderImage`/atlas 类型,暂记为后续项)→ `write_texture`。gpui_wgpu atlas 的 identity 格式二次拷贝(`swizzle_upload_data` 的 `to_vec()`)已移除:非 swizzle 格式直接用调用方切片上传。真·零拷贝上屏的可行路径是 macOS IOSurface 跨进程共享 + `SurfaceSource` 通道,工作量与平台耦合大,单独立项再议。
## 1. 目标(用户原文要求)
+1 -1
Submodule gpui updated: 050d5ba22d...a761d96c82
+30
View File
@@ -1555,7 +1555,37 @@ impl<E: AppEngine> OakApp<E> {
/// `WindowHandle::update`): the nested `update_window` below would fail
/// and the modal would silently not open. Drive the root entity instead
/// (menu actions and entity updates are fine).
/// Builds and installs a modal. When the caller is already inside a
/// window update (an action listener / menu dispatch / tick), a nested
/// `update_window` would silently fail and drop the dialog — the phase-7
/// fix for Preferences / Action Search, applied centrally: probe first,
/// and defer the build to the end of the current app update when nested.
/// The fast path stays synchronous so callers can rely on `self.modal`
/// right after the call.
fn spawn_modal(
&mut self,
cx: &mut Context<Self>,
build: impl FnOnce(&mut Window, &mut App) -> ModalState<E> + 'static,
) {
let handle = cx.windows().first().copied();
let nested = match handle {
Some(handle) => cx.update_window(handle, |_root, _window, _app| ()).is_err(),
None => false,
};
if nested {
let this = cx.weak_entity();
cx.defer(move |app| {
let _ = this.update(app, |this, cx| this.spawn_modal_now(cx, build));
});
return;
}
self.spawn_modal_now(cx, build);
}
/// Builds and installs a modal immediately (the deferred half of
/// [`Self::spawn_modal`]; also called directly when the caller already
/// runs outside a window update).
fn spawn_modal_now(
&mut self,
cx: &mut Context<Self>,
build: impl FnOnce(&mut Window, &mut App) -> ModalState<E>,
+35 -1
View File
@@ -94,7 +94,15 @@ pub fn preview_footage_media(
f: &oaknode::footage::FootageBehavior,
is_video: bool,
) -> (String, i32) {
let original_stream = if is_video { 0 } else { 1 };
// The original media decodes from the footage's actual first stream of
// the kind (C++ maps per-stream); the 0/1 fallbacks cover footage
// whose streams were never probed (legacy project files).
let original_stream = f
.streams
.iter()
.find(|s| s.is_video == is_video)
.map(|s| s.index)
.unwrap_or(if is_video { 0 } else { 1 });
let original = (f.filename.clone(), original_stream);
if !use_proxy_media() || !f.proxy_enabled || f.proxy.is_empty() {
return original;
@@ -886,6 +894,32 @@ mod tests {
let _ = std::fs::remove_file(&media);
}
/// The original media decodes from the footage's actual first stream of
/// the kind (not the hardcoded 0/1 of a typical layout): a file whose
/// video stream is not stream 0 must still decode its own video when
/// the proxy switch is off or the proxy is not ready.
#[test]
fn preview_media_uses_the_probed_stream_indices() {
let stream = |index: i32, is_video: bool| oaknode::footage::StreamInfo {
index,
is_video,
video: None,
audio: None,
duration: oakcore_rs::Rational::new(0, 1),
};
// An audio-first container: audio at 0, video at 1… plus a second
// audio track at 2. The video must come from stream 1 and the audio
// from stream 0, not the legacy 0/1 assumption.
let mut f = oaknode::footage::FootageBehavior::new("/tmp/audio-first.mov");
f.streams = vec![stream(0, false), stream(1, true), stream(2, false)];
assert_eq!(preview_footage_media(&f, true).1, 1);
assert_eq!(preview_footage_media(&f, false).1, 0);
// Unprobed footage (no stream metadata) keeps the 0/1 fallbacks.
let unprobed = oaknode::footage::FootageBehavior::new("/tmp/legacy.mov");
assert_eq!(preview_footage_media(&unprobed, true).1, 0);
assert_eq!(preview_footage_media(&unprobed, false).1, 1);
}
/// The multicam angle montage ([`single_track_video_montage`]) carries
/// ONLY the clip on the requested track — the whole-stack `video_montage`
/// is the parity reference. This is the montage the angle-frame ticket