mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 12:31:27 +00:00
Viewer exposure on the full float range of 32 bpc frames (#2937)
* Viewer exposure on the full float range of 32 bpc frames (#2797) 32 bpc viewer frames on the desktop are rendered without the HDR step (skip_hdr_step, #2916) and kept as half floats in the RAM preview. A display pass on egui-wgpu's device turns each frame into one RGBA8 texture with mips per viewer, in this order: decode, exposure in linear light, Compand / Tone Map, encode, display colour, Show Channel, rounding. Before, exposure was an 8-bit lookup applied after tone mapping, clamping and the display conversion, so -2 EV could not bring values above 1.0 back. - engine: viewer::DisplayPass describes the pass as data; its pixel() is the CPU fallback (no device) and the shader's reference. It replaces exposure_lut / display_transform. FrameEncoding records a frame's curve and the HDR step it still needs. 32 bpc snapshots are taken without the HDR step; snapshot_rgba8 applies it. - ui-egui: FrameImage::CpuF16 (half crate, F16C at run time), byte accounting at 8 bytes a pixel in the cache and the prefetch estimates; display_gpu.rs + display_gpu.wgsl; main viewer, snapshot and passive viewers draw through the pass; the 8-bit image other panels read is made on demand with the HDR step; the Info panel shows float values. - Half-float frames are not written to the disk cache yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Viewer display pass: f16 disk cache entries, window-device gating, review fixes - Disk cache: half-float frame entries (format byte in the entry header, byte planes + LZ4); 8-bit entries keep format 0 and stay valid. 32 bpc viewer frames are written and read again, and the timeline's disk bar sees them. A damaged half-float entry is deleted. - The display pass runs on the window's egui-wgpu device, with or without the GPU compositor (still building, failed, GL fallback). Its pipelines are made inside error scopes; a backend that rejects them gets the CPU pass instead of a blank viewer. - A new LUT of the same size (another ICC profile, Preserve RGB, another display) runs the pass again. - f32 to f16: one pass per L1-sized block, the NaN / infinity fix-up on the bits, no fill of the buffer (4K: 4.7 ms, as the 8-bit conversion). - Mips of odd-sized levels include the last column / row; the snapshot's half floats and texture go with the snapshot; no placeholder texture ids; u64 size checks. - Tests: LUT profiles and negative values against the shader, Bake reruns, the passive viewer's colour, f16 disk round trips; exact comparisons allow 1 LSB on software rasterizers. Docs: the display pass and the f16 disk entries in architecture.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Viewer f16 disk-cache test: temp dir removed on failure too, wait for the queued disk write Review points: the test's directory was only removed by its last statement (now tempfile::TempDir), and the frame is released before its disk write is queued, so flush() could return early (now waits for the entry). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Viewer: oversized half-float frames use the CPU display pass, so exposure/HDR come before the averaging down Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Viewer: fmt and a debug assert for the half-float upload path Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Disk cache: persist_missing also writes half-float frames Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Passive viewer: oversized half-float frames take the CPU display pass instead of leaving a stale texture Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8948a7cdc9
commit
fdcc715829
31 files changed
+3081
-250
No files matched your search
@@ -159,9 +159,14 @@ fn take_snapshot(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
let t = s.time_of(cid);
|
||||
let scale = f_p(p, "scale").unwrap_or(1.0).clamp(0.05, 1.0);
|
||||
let img = s.render(cid, t, RenderOpts { scale, guides: true, view: s.view_camera(cid).filter(|_| comp.has_3d()), ..Default::default() });
|
||||
// 32 bpc: without the HDR step, so viewer exposure reaches the over-range values (the
|
||||
// display pass applies the step, as for viewer frames).
|
||||
let skip_hdr_step = s.project.settings.bit_depth.levels().is_none();
|
||||
let opts = RenderOpts { scale, guides: true, view: s.view_camera(cid).filter(|_| comp.has_3d()), skip_hdr_step, ..Default::default() };
|
||||
let img = s.render(cid, t, opts);
|
||||
let (w, h) = (img.width, img.height);
|
||||
s.snapshot = Some(Snapshot { comp: cid, time: t, scale, image: std::sync::Arc::new(img) });
|
||||
let encoding = crate::viewer::FrameEncoding::of(&s.project.settings, skip_hdr_step);
|
||||
s.snapshot = Some(Snapshot { comp: cid, time: t, scale, image: std::sync::Arc::new(img), encoding });
|
||||
s.events.push(crate::Event::ProjectChanged { revision: s.revision });
|
||||
Ok(json!({"width": w, "height": h, "time": t.seconds()}))
|
||||
}
|
||||
@@ -298,15 +303,17 @@ fn new_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"layer": lid.0, "group": group, "path": path_uid}))
|
||||
}
|
||||
|
||||
/// Snapshot image as straight 8-bit RGBA rows (for frontends / `--out`).
|
||||
/// Snapshot image as straight 8-bit RGBA rows (for frontends / `--out`), with the HDR step a
|
||||
/// 32 bpc snapshot was taken without.
|
||||
pub fn snapshot_rgba8(s: &Session) -> Option<(u32, u32, Vec<u8>)> {
|
||||
let snap = s.snapshot.as_ref()?;
|
||||
let img = &snap.image;
|
||||
let mut out = Vec::with_capacity(img.data.len() * 4);
|
||||
for p in &img.data {
|
||||
let a = p[3].clamp(0.0, 1.0);
|
||||
let un = |c: f32| if a > 0.0 { (c / a).clamp(0.0, 1.0) } else { 0.0 };
|
||||
out.extend([un(p[0]), un(p[1]), un(p[2]), a].map(|v| (v * 255.0 + 0.5) as u8));
|
||||
let un = |c: f32| if a > 0.0 { c / a } else { 0.0 };
|
||||
let c = snap.encoding.finish([un(p[0]), un(p[1]), un(p[2])]).map(|v| v.clamp(0.0, 1.0));
|
||||
out.extend([c[0], c[1], c[2], a].map(|v| (v * 255.0 + 0.5) as u8));
|
||||
}
|
||||
Some((img.width, img.height, out))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user