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:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user