-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix: prevent stale-config frames from appearing in exported video near trim boundaries #1711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4c85528
20eafba
97e2490
d0f553c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -365,7 +365,12 @@ function Inner() { | |
| setEditorState("playbackTime", payload.playhead_position / FPS); | ||
| }); | ||
|
|
||
| let skipRenderFrameForConfigUpdate = false; | ||
|
|
||
| const emitRenderFrame = (time: number) => { | ||
| if (skipRenderFrameForConfigUpdate) { | ||
| return; | ||
| } | ||
| if (!editorState.playing) { | ||
| events.renderFrameEvent.emit({ | ||
| frame_number: Math.max(Math.floor(time * FPS), 0), | ||
|
|
@@ -390,33 +395,6 @@ function Inner() { | |
| return editorState.playbackTime; | ||
| }); | ||
|
|
||
| createEffect( | ||
| on( | ||
| () => [frameNumberToRender(), previewResolutionBase()], | ||
| ([number]) => { | ||
| if (editorState.playing) return; | ||
| renderFrame(number as number); | ||
| }, | ||
| { defer: false }, | ||
| ), | ||
| ); | ||
|
|
||
| createEffect( | ||
| on(isExportMode, (exportMode, prevExportMode) => { | ||
| if (prevExportMode === true && exportMode === false) { | ||
| emitRenderFrame(frameNumberToRender()); | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| createEffect( | ||
| on(isCropMode, (cropMode, prevCropMode) => { | ||
| if (prevCropMode === true && cropMode === false) { | ||
| emitRenderFrame(frameNumberToRender()); | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| const doConfigUpdate = async (time: number) => { | ||
| const config = getPreviewProjectConfig(project, editorState); | ||
| const frameNumber = Math.max(Math.floor(time * FPS), 0); | ||
|
|
@@ -441,6 +419,7 @@ function Inner() { | |
| throttledConfigUpdate(time); | ||
| trailingConfigUpdate(time); | ||
| }; | ||
|
|
||
| createEffect( | ||
| on( | ||
| () => { | ||
|
|
@@ -451,12 +430,43 @@ function Inner() { | |
| }; | ||
| }, | ||
| () => { | ||
| skipRenderFrameForConfigUpdate = true; | ||
| queueMicrotask(() => { | ||
| skipRenderFrameForConfigUpdate = false; | ||
| }); | ||
| updateConfigAndRender(frameNumberToRender()); | ||
| }, | ||
| { defer: true }, | ||
| ), | ||
| ); | ||
|
|
||
| createEffect( | ||
| on( | ||
| () => [frameNumberToRender(), previewResolutionBase()], | ||
| ([number]) => { | ||
| if (editorState.playing) return; | ||
| renderFrame(number as number); | ||
| }, | ||
| { defer: false }, | ||
| ), | ||
| ); | ||
|
Comment on lines
+443
to
+452
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In practice this is acceptable — Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src/routes/editor/Editor.tsx
Line: 444-453
Comment:
**`trailingRenderFrame` is not protected by the skip flag**
`renderFrame()` at line 449 enqueues both `throttledRenderFrame` (leading-edge, synchronously guarded by the flag) and `trailingRenderFrame` (a debounce with delay `1000/FPS + 16` ms). By the time the trailing debounce fires, the `queueMicrotask` has already cleared `skipRenderFrameForConfigUpdate`, so the trailing render is never blocked by the guard.
In practice this is acceptable — `throttledConfigUpdate` dispatches the IPC on the leading edge and Tauri IPC is typically sub-millisecond, well within the ~49 ms trailing window. However, this is an implicit timing assumption: under system load, if the IPC call takes longer than the debounce window, `renderFrameEvent` can still arrive at Rust before `updateProjectConfigInMemory` completes — recreating the same race this PR intends to fix. Consider adding a comment documenting why the trailing window is safe, or coordinating the trailing render with the in-flight config promise.
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| createEffect( | ||
| on(isExportMode, (exportMode, prevExportMode) => { | ||
| if (prevExportMode === true && exportMode === false) { | ||
| emitRenderFrame(frameNumberToRender()); | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| createEffect( | ||
| on(isCropMode, (cropMode, prevCropMode) => { | ||
| if (prevCropMode === true && cropMode === false) { | ||
| emitRenderFrame(frameNumberToRender()); | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| const fullscreenMode = () => { | ||
| if (isExportMode()) return "export" as const; | ||
| return null; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
emitRenderFrameThe
skipRenderFrameForConfigUpdateflag is cleared in two places: eagerly insideemitRenderFrameat line 372, and again by thequeueMicrotaskat line 436. The reset insideemitRenderFrameis redundant since the microtask already owns cleanup.More importantly, this early reset means that if
emitRenderFrameis invoked more than once synchronously within the same reactive flush — e.g., the leading-edge throttled render fires and anisExportMode/isCropModeexit handler callsemitRenderFramedirectly before the microtask runs — only the first call is blocked. The flag is cleared prematurely and the second call bypasses the guard beforeupdateProjectConfigInMemoryhas completed. While the current UI should prevent those callers from coinciding, letting the microtask be the sole owner of flag cleanup is simpler and more defensive:Prompt To Fix With AI