-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Faster playback prep, export pipeline, and GIF frame handling #1682
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
Changes from all commits
d1e2d9f
6c5046d
a37e070
e317617
68d6d44
1a6d6d9
5f71b8c
6749644
748cebc
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 | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -100,7 +100,6 @@ impl GifEncoderWrapper { | |||||||||||
| }) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /// Add a frame to the GIF | ||||||||||||
| pub fn add_frame( | ||||||||||||
| &mut self, | ||||||||||||
| frame_data: &[u8], | ||||||||||||
|
|
@@ -115,44 +114,37 @@ impl GifEncoderWrapper { | |||||||||||
| .as_mut() | ||||||||||||
| .ok_or(GifEncodingError::EncoderFinished)?; | ||||||||||||
|
|
||||||||||||
| // Calculate expected size | ||||||||||||
| let expected_bytes_per_row = (self.width as usize) * 4; // RGBA | ||||||||||||
| let expected_total_bytes = expected_bytes_per_row * (self.height as usize); | ||||||||||||
| let w = self.width as usize; | ||||||||||||
| let h = self.height as usize; | ||||||||||||
| let expected_bytes_per_row = w * 4; | ||||||||||||
|
|
||||||||||||
| // Validate frame data size | ||||||||||||
| if bytes_per_row < expected_bytes_per_row || frame_data.len() < expected_total_bytes { | ||||||||||||
| if bytes_per_row < expected_bytes_per_row | ||||||||||||
| || frame_data.len() < bytes_per_row * h.saturating_sub(1) + expected_bytes_per_row | ||||||||||||
| { | ||||||||||||
| return Err(GifEncodingError::InvalidFrameData); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Convert RGBA data to gifski's expected format | ||||||||||||
| let mut rgba_pixels = Vec::with_capacity(self.width as usize * self.height as usize); | ||||||||||||
|
|
||||||||||||
| for y in 0..self.height { | ||||||||||||
| let src_row_start = (y as usize) * bytes_per_row; | ||||||||||||
|
|
||||||||||||
| for x in 0..self.width { | ||||||||||||
| let pixel_start = src_row_start + (x as usize) * 4; | ||||||||||||
|
|
||||||||||||
| if pixel_start + 3 < frame_data.len() { | ||||||||||||
| let r = frame_data[pixel_start]; | ||||||||||||
| let g = frame_data[pixel_start + 1]; | ||||||||||||
| let b = frame_data[pixel_start + 2]; | ||||||||||||
| let a = frame_data[pixel_start + 3]; | ||||||||||||
|
|
||||||||||||
| rgba_pixels.push(RGBA8::new(r, g, b, a)); | ||||||||||||
| } else { | ||||||||||||
| return Err(GifEncodingError::InvalidFrameData); | ||||||||||||
| } | ||||||||||||
| let img = if bytes_per_row == expected_bytes_per_row { | ||||||||||||
|
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. This |
||||||||||||
| let pixel_count = w * h; | ||||||||||||
| let byte_slice = &frame_data[..pixel_count * 4]; | ||||||||||||
| let pixels: &[RGBA8] = unsafe { | ||||||||||||
|
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. Bug: Unnecessary unsafe casts when safe
|
||||||||||||
| What fails | Two unsafe blocks use raw pointer casts to reinterpret &[u8] as &[RGBA8], but the already-imported rgb crate provides a safe as_rgba() method that does the same thing. |
| Result | Unnecessary unsafe code that expands audit surface and could silently become UB if repr assumptions on RGBA8 ever changed. |
| Expected | Use the safe rgb::FromSlice::as_rgba() API which performs the same reinterpretation with compile-time safety guarantees. |
| Impact | Unnecessary unsafe blocks increase code review burden and risk of future unsoundness. The safe alternative is a one-line drop-in replacement from an existing dependency. |
How to reproduce
Review crates/enc-gif/src/lib.rs and note the two unsafe { std::slice::from_raw_parts(...) } blocks. Then check rgb crate docs for FromSlice::as_rgba().Patch Details
- let pixels: &[RGBA8] = unsafe {
- std::slice::from_raw_parts(byte_slice.as_ptr().cast::<RGBA8>(), pixel_count)
- };
+ let pixels: &[RGBA8] = byte_slice.as_rgba();AI Fix Prompt
Fix this issue: Unnecessary unsafe casts when safe `rgb::FromSlice::as_rgba()` exists. Expands audit surface for no benefit. Replace ``slice`::from_raw_parts` with `byte_slice.as_rgba()`.
Location: crates/enc-gif/src/lib.rs (lines 130)
Problem: Two unsafe blocks use raw pointer casts to reinterpret &[u8] as &[RGBA8], but the already-imported rgb crate provides a safe as_rgba() method that does the same thing.
Current behavior: Unnecessary unsafe code that expands audit surface and could silently become UB if repr assumptions on RGBA8 ever changed.
Expected: Use the safe rgb::FromSlice::as_rgba() API which performs the same reinterpretation with compile-time safety guarantees.
Steps to reproduce: Review crates/enc-gif/src/lib.rs and note the two unsafe { std::slice::from_raw_parts(...) } blocks. Then check rgb crate docs for FromSlice::as_rgba().
Provide a code fix.
Tip: Reply with @paragon-run to automatically fix this issue
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.
Missing safety comments on
unsafe blocks
Both unsafe { std::slice::from_raw_parts(...) } blocks lack any explanation of their invariants. While the casts are sound — RGBA8 is #[repr(C)] with four u8 fields (alignment 1, size 4), so reinterpreting a *const u8 pointer is valid — the reasoning should be documented to prevent future regressions.
Consider adding a // SAFETY: comment before each block, e.g.:
// SAFETY: `RGBA8` is `#[repr(C)]` with four consecutive `u8` fields (size 4,
// alignment 1), so it can safely alias a `[u8; 4]` slice at the same pointer.
// Bounds were validated above: `byte_slice` covers exactly `pixel_count * 4` bytes.
let pixels: &[RGBA8] = unsafe {
std::slice::from_raw_parts(byte_slice.as_ptr().cast::<RGBA8>(), pixel_count)
};The same reasoning applies to the strided else branch at line 139–140.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/enc-gif/src/lib.rs
Line: 127-144
Comment:
**Missing safety comments on `unsafe` blocks**
Both `unsafe { std::slice::from_raw_parts(...) }` blocks lack any explanation of their invariants. While the casts are sound — `RGBA8` is `#[repr(C)]` with four `u8` fields (alignment 1, size 4), so reinterpreting a `*const u8` pointer is valid — the reasoning should be documented to prevent future regressions.
Consider adding a `// SAFETY:` comment before each block, e.g.:
```rust
// SAFETY: `RGBA8` is `#[repr(C)]` with four consecutive `u8` fields (size 4,
// alignment 1), so it can safely alias a `[u8; 4]` slice at the same pointer.
// Bounds were validated above: `byte_slice` covers exactly `pixel_count * 4` bytes.
let pixels: &[RGBA8] = unsafe {
std::slice::from_raw_parts(byte_slice.as_ptr().cast::<RGBA8>(), pixel_count)
};
```
The same reasoning applies to the strided `else` branch at line 139–140.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| Original file line number | Diff line number | Diff line change | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -50,17 +50,17 @@ cargo run -p cap-export --example export-benchmark-runner -- full --duration 60 | ||||||||||
|
|
|||||||||||
| <!-- EXPORT_BENCHMARK_RESULTS_START --> | |||||||||||
|
|
|||||||||||
| ### Benchmark Run: 2026-02-16 11:02:26 UTC | |||||||||||
| ### Benchmark Run: 2026-03-25 13:12:31 UTC | |||||||||||
|
|
|||||||||||
| *Local time: 2026-02-16 11:02:26* | |||||||||||
| *Local time: 2026-03-25 13:12:31* | |||||||||||
|
|
|||||||||||
| **Overall Result:** ALL PASS (9/9) | |||||||||||
|
|
|||||||||||
| **Test Video:** 72s at 1920x1080 30fps | |||||||||||
| **Test Video:** 30s at 1920x1080 30fps | |||||||||||
|
|
|||||||||||
| **Notes:** Final calibration: encoder_efficiency=0.5 applied, FPS tapering, real-world data | |||||||||||
| **Notes:** Post-optimization: trimmed macOS encoder priority, increased NV12 render channel 2->8, optimized GIF add_frame | |||||||||||
|
|
|||||||||||
| **Command:** `cargo run -p cap-export --example export-benchmark-runner -- mp4-only --duration 72 --recording-path /Users/richie/Library/Application Support/so.cap.desktop.dev/recordings/Odyssey G93SC (Display) 2026-02-16 10.06 AM.cap --benchmark-output` | |||||||||||
| **Command:** `cargo run -p cap-export --example export-benchmark-runner -- mp4-only --duration 30 --benchmark-output` | |||||||||||
|
|
|||||||||||
| <details> | |||||||||||
| <summary>System Information</summary> | |||||||||||
|
|
@@ -74,36 +74,36 @@ cargo run -p cap-export --example export-benchmark-runner -- full --duration 60 | ||||||||||
|
|
|||||||||||
| | Preset | Time(s) | FPS | Size(MB) | Estimated(MB) | Size Err(%) | Time Est(s) | Time Err(%) | Status | | |||||||||||
| |--------|---------|-----|----------|---------------|-------------|-------------|-------------|--------| | |||||||||||
| | MP4 720p/30fps/Maximum | 7.58 | 283.4 | 35.79 | 36.22 | +1.2 | 7.41 | -2.3 | PASS | | |||||||||||
| | MP4 720p/30fps/Social | 7.78 | 276.2 | 18.93 | 18.52 | -2.2 | 7.41 | -4.8 | PASS | | |||||||||||
| | MP4 720p/30fps/Web | 7.03 | 305.6 | 12.13 | 10.26 | -15.4 | 7.41 | +5.4 | PASS | | |||||||||||
| | MP4 1080p/30fps/Maximum | 7.66 | 280.3 | 80.27 | 80.46 | +0.2 | 7.41 | -3.4 | PASS | | |||||||||||
| | MP4 1080p/30fps/Social | 8.62 | 249.2 | 41.19 | 40.64 | -1.3 | 7.41 | -14.1 | PASS | | |||||||||||
| | MP4 1080p/30fps/Web | 7.50 | 286.3 | 23.37 | 22.06 | -5.6 | 7.41 | -1.3 | PASS | | |||||||||||
| | MP4 1080p/60fps/Maximum | 15.15 | 283.5 | 127.65 | 128.25 | +0.5 | 14.81 | -2.2 | PASS | | |||||||||||
| | MP4 4K/30fps/Maximum | 20.22 | 106.3 | 319.82 | 319.39 | -0.1 | 12.27 | -39.3 | PASS | | |||||||||||
| | MP4 4K/30fps/Social | 12.26 | 175.2 | 161.26 | 160.11 | -0.7 | 12.27 | +0.1 | PASS | | |||||||||||
| | MP4 720p/30fps/Maximum | 2.48 | 362.3 | 6.01 | 15.17 | +152.6 | 3.10 | +24.9 | PASS | | |||||||||||
| | MP4 720p/30fps/Social | 2.59 | 347.2 | 5.98 | 7.76 | +29.7 | 3.10 | +19.7 | PASS | | |||||||||||
| | MP4 720p/30fps/Web | 2.57 | 350.3 | 5.71 | 4.30 | -24.7 | 3.10 | +20.8 | PASS | | |||||||||||
| | MP4 1080p/30fps/Maximum | 3.12 | 288.8 | 3.99 | 33.71 | +745.9 | 3.10 | -0.4 | PASS | | |||||||||||
| | MP4 1080p/30fps/Social | 3.31 | 272.3 | 3.95 | 17.03 | +330.8 | 3.10 | -6.1 | PASS | | |||||||||||
| | MP4 1080p/30fps/Web | 3.31 | 271.9 | 3.93 | 9.24 | +135.0 | 3.10 | -6.3 | PASS | | |||||||||||
| | MP4 1080p/60fps/Maximum | 5.93 | 303.8 | 5.50 | 53.74 | +876.3 | 6.21 | +4.8 | PASS | | |||||||||||
| | MP4 4K/30fps/Maximum | 8.28 | 108.7 | 6.63 | 133.83 | +1920.0 | 5.14 | -37.9 | PASS | | |||||||||||
| | MP4 4K/30fps/Social | 8.27 | 108.8 | 6.54 | 67.09 | +926.1 | 5.14 | -37.8 | PASS | | |||||||||||
|
|
|||||||||||
| #### Estimation Accuracy | |||||||||||
|
|
|||||||||||
| - **MP4 Size**: avg error -2.6%, avg |error| 3.0% | |||||||||||
| - **MP4 Time**: avg error -6.9%, avg |error| 8.1% | |||||||||||
| - **MP4 Size**: avg error +565.7%, avg |error| 571.2% | |||||||||||
| - **MP4 Time**: avg error -2.0%, avg |error| 17.6% | |||||||||||
|
|
|||||||||||
| #### Calibration Data | |||||||||||
|
|
|||||||||||
| Use these actual-vs-estimated ratios to tune the estimation algorithm: | |||||||||||
|
|
|||||||||||
| | Preset | Actual(MB) | Estimated(MB) | Ratio (actual/est) | Suggested BPP Multiplier | | |||||||||||
| |--------|------------|---------------|--------------------|--------------------------| | |||||||||||
|
Comment on lines
87
to
97
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.
The new benchmark run was against a 30-second synthetic test clip, which compresses far more efficiently than real screen recordings. As a result, the "Calibration Data" table suggests BPP multipliers that are 5–20× lower than the real-world values:
If these suggestions were ever applied to the estimation algorithm, size estimates on real recordings would be wildly wrong in the other direction. It may be worth either (a) adding a prominent warning in the Calibration Data section that these ratios are synthetic-only, or (b) leaving the real-world calibration section (from the 2026-02-16 session) as the canonical reference and omitting the "Suggested BPP Multiplier" column from synthetic runs entirely. Prompt To Fix With AIThis is a comment left during a code review.
Path: crates/export/EXPORT-BENCHMARKS.md
Line: 87-97
Comment:
**Calibration data from synthetic content will break real-world size estimates**
The new benchmark run was against a 30-second synthetic test clip, which compresses far more efficiently than real screen recordings. As a result, the "Calibration Data" table suggests BPP multipliers that are 5–20× lower than the real-world values:
| Preset | Current multiplier | Suggested multiplier (this run) |
|---|---|---|
| MP4 1080p Maximum | 0.30 | 0.0355 |
| MP4 4K Maximum | 0.30 | 0.0149 |
If these suggestions were ever applied to the estimation algorithm, size estimates on real recordings would be wildly wrong in the other direction. It may be worth either (a) adding a prominent warning in the Calibration Data section that these ratios are synthetic-only, or (b) leaving the real-world calibration section (from the 2026-02-16 session) as the canonical reference and omitting the "Suggested BPP Multiplier" column from synthetic runs entirely.
How can I resolve this? If you propose a fix, please make it concise. |
|||||||||||
| | MP4 720p/30fps/Maximum | 35.79 | 36.22 | 0.9882 | 0.2965 (current: 0.30) | | |||||||||||
| | MP4 720p/30fps/Social | 18.93 | 18.52 | 1.0224 | 0.1534 (current: 0.15) | | |||||||||||
| | MP4 720p/30fps/Web | 12.13 | 10.26 | 1.1827 | 0.0946 (current: 0.08) | | |||||||||||
| | MP4 1080p/30fps/Maximum | 80.27 | 80.46 | 0.9976 | 0.2993 (current: 0.30) | | |||||||||||
| | MP4 1080p/30fps/Social | 41.19 | 40.64 | 1.0134 | 0.1520 (current: 0.15) | | |||||||||||
| | MP4 1080p/30fps/Web | 23.37 | 22.06 | 1.0593 | 0.0847 (current: 0.08) | | |||||||||||
| | MP4 1080p/60fps/Maximum | 127.65 | 128.25 | 0.9953 | 0.2986 (current: 0.30) | | |||||||||||
| | MP4 4K/30fps/Maximum | 319.82 | 319.39 | 1.0013 | 0.3004 (current: 0.30) | | |||||||||||
| | MP4 4K/30fps/Social | 161.26 | 160.11 | 1.0072 | 0.1511 (current: 0.15) | | |||||||||||
| | MP4 720p/30fps/Maximum | 6.01 | 15.17 | 0.3958 | 0.1187 (current: 0.30) | | |||||||||||
| | MP4 720p/30fps/Social | 5.98 | 7.76 | 0.7709 | 0.1156 (current: 0.15) | | |||||||||||
| | MP4 720p/30fps/Web | 5.71 | 4.30 | 1.3286 | 0.1063 (current: 0.08) | | |||||||||||
| | MP4 1080p/30fps/Maximum | 3.99 | 33.71 | 0.1182 | 0.0355 (current: 0.30) | | |||||||||||
| | MP4 1080p/30fps/Social | 3.95 | 17.03 | 0.2321 | 0.0348 (current: 0.15) | | |||||||||||
| | MP4 1080p/30fps/Web | 3.93 | 9.24 | 0.4255 | 0.0340 (current: 0.08) | | |||||||||||
| | MP4 1080p/60fps/Maximum | 5.50 | 53.74 | 0.1024 | 0.0307 (current: 0.30) | | |||||||||||
| | MP4 4K/30fps/Maximum | 6.63 | 133.83 | 0.0495 | 0.0149 (current: 0.30) | | |||||||||||
| | MP4 4K/30fps/Social | 6.54 | 67.09 | 0.0975 | 0.0146 (current: 0.15) | | |||||||||||
|
|
|||||||||||
| --- | |||||||||||
|
|
|||||||||||
|
|
|||||||||||
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.
Bug: GIF validation formula rejects valid h==0 frames
GIF validation formula rejects valid h==0 frames.
saturating_sub(1)still requiresw*4bytes for zero rows. Guard withh > 0before size check.View Details
Location:
crates/enc-gif/src/lib.rs(lines 121)Analysis
GIF validation formula rejects valid h==0 frames.
saturating_sub(1)still requiresw*4bytes forHow to reproduce
Call add_frame with height=0 and an empty frame_data buffer. The validation check will return InvalidFrameData even though 0 rows require 0 bytes of data.Patch Details
AI Fix Prompt
Tip: Reply with
@paragon-runto automatically fix this issue