From 759e41d84a221eeb6c45f79655495f17a7832760 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 4 Mar 2026 11:25:15 -0500 Subject: [PATCH] fix color space for raster editing --- .../lightningbeam-editor/src/gpu_brush.rs | 240 +++++++++--------- .../src/panes/shaders/canvas_blit.wgsl | 39 +-- .../lightningbeam-editor/src/panes/stage.rs | 57 +++-- 3 files changed, 169 insertions(+), 167 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/gpu_brush.rs b/lightningbeam-ui/lightningbeam-editor/src/gpu_brush.rs index 81465eb..092cbd8 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/gpu_brush.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/gpu_brush.rs @@ -94,6 +94,7 @@ impl CanvasPair { /// in `raw_pixels` / PNG files). The values are decoded to linear premultiplied /// before being written to the canvas, which operates entirely in linear space. pub fn upload(&self, queue: &wgpu::Queue, pixels: &[u8]) { + eprintln!("[CANVAS] upload: {}x{} pixels={}", self.width, self.height, pixels.len()); // Decode sRGB-premultiplied → linear premultiplied for the GPU canvas. let linear: Vec = pixels.chunks_exact(4).flat_map(|p| { let r = (srgb_to_linear(p[0] as f32 / 255.0) * 255.0 + 0.5) as u8; @@ -268,147 +269,156 @@ impl GpuBrushEngine { let needs_new = self.canvases.get(&keyframe_id) .map_or(true, |c| c.width != width || c.height != height); if needs_new { + eprintln!("[CANVAS] ensure_canvas: creating new CanvasPair for kf={:?} {}x{}", keyframe_id, width, height); self.canvases.insert(keyframe_id, CanvasPair::new(device, width, height)); + } else { + eprintln!("[CANVAS] ensure_canvas: reusing existing CanvasPair for kf={:?}", keyframe_id); } self.canvases.get_mut(&keyframe_id).unwrap() } /// Dispatch the brush compute shader for `dabs` onto the canvas of `keyframe_id`. /// - /// * Pre-fills `dst` from `src` so untouched pixels are preserved. - /// * Dispatches the compute shader. - /// * Swaps src/dst so the just-written texture becomes the new source. + /// Each dab is dispatched as a separate copy+compute+swap so that every dab + /// reads the result of the previous one. This is required for the smudge tool: + /// if all dabs were batched into one dispatch they would all read the pre-batch + /// canvas state, breaking the carry-forward that makes smudge drag pixels along. /// - /// `dab_bbox` is `(x0, y0, x1, y1)` — the union bounding box of all dabs. - /// If `dabs` is empty or the bbox is invalid, does nothing. + /// `dab_bbox` is the union bounding box (unused here; kept for API compat). + /// If `dabs` is empty, does nothing. pub fn render_dabs( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, keyframe_id: Uuid, dabs: &[GpuDab], - bbox: (i32, i32, i32, i32), + _bbox: (i32, i32, i32, i32), canvas_w: u32, canvas_h: u32, ) { - if dabs.is_empty() || bbox.0 == i32::MAX { return; } + if dabs.is_empty() { return; } - let canvas = match self.canvases.get_mut(&keyframe_id) { - Some(c) => c, - None => return, - }; + if !self.canvases.contains_key(&keyframe_id) { return; } - // Clamp bbox to canvas bounds - let x0 = bbox.0.max(0) as u32; - let y0 = bbox.1.max(0) as u32; - let x1 = (bbox.2.min(canvas_w as i32 - 1)).max(0) as u32; - let y1 = (bbox.3.min(canvas_h as i32 - 1)).max(0) as u32; - if x1 < x0 || y1 < y0 { return; } - - let bbox_w = x1 - x0 + 1; - let bbox_h = y1 - y0 + 1; - - // --- Pre-fill dst from src: copy the ENTIRE canvas so every pixel outside - // the dab bounding box is preserved across the ping-pong swap. - // Copying only the bbox would leave dst with data from two frames ago - // in all other regions, causing missing dabs on alternating frames. --- - let mut copy_encoder = device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { label: Some("canvas_copy_encoder") }, - ); let full_extent = wgpu::Extent3d { - width: canvas.width, - height: canvas.height, + width: self.canvases[&keyframe_id].width, + height: self.canvases[&keyframe_id].height, depth_or_array_layers: 1, }; - copy_encoder.copy_texture_to_texture( - wgpu::TexelCopyTextureInfo { - texture: canvas.src(), - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - wgpu::TexelCopyTextureInfo { - texture: canvas.dst(), - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - full_extent, - ); - queue.submit(Some(copy_encoder.finish())); - // --- Upload dab data and params --- - let dab_bytes = bytemuck::cast_slice(dabs); - let dab_buf = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("dab_storage_buf"), - size: dab_bytes.len() as u64, - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - queue.write_buffer(&dab_buf, 0, dab_bytes); + eprintln!("[DAB] render_dabs keyframe={:?} count={}", keyframe_id, dabs.len()); + for dab in dabs { + // Per-dab bounding box + let r_fringe = dab.radius + 1.0; + let dx0 = (dab.x - r_fringe).floor() as i32; + let dy0 = (dab.y - r_fringe).floor() as i32; + let dx1 = (dab.x + r_fringe).ceil() as i32; + let dy1 = (dab.y + r_fringe).ceil() as i32; - let params = DabParams { - bbox_x0: x0 as i32, - bbox_y0: y0 as i32, - bbox_w, - bbox_h, - num_dabs: dabs.len() as u32, - canvas_w, - canvas_h, - _pad: 0, - }; - let params_buf = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("dab_params_buf"), - size: std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - queue.write_buffer(¶ms_buf, 0, bytemuck::bytes_of(¶ms)); + let x0 = dx0.max(0) as u32; + let y0 = dy0.max(0) as u32; + let x1 = (dx1.min(canvas_w as i32 - 1)).max(0) as u32; + let y1 = (dy1.min(canvas_h as i32 - 1)).max(0) as u32; + if x1 < x0 || y1 < y0 { continue; } - let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("brush_dab_bg"), - layout: &self.compute_bg_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: dab_buf.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: params_buf.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::TextureView(canvas.src_view()), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::TextureView(canvas.dst_view()), - }, - ], - }); + let bbox_w = x1 - x0 + 1; + let bbox_h = y1 - y0 + 1; - // --- Dispatch --- - let mut compute_encoder = device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { label: Some("brush_dab_encoder") }, - ); - { - let mut pass = compute_encoder.begin_compute_pass( - &wgpu::ComputePassDescriptor { - label: Some("brush_dab_pass"), - timestamp_writes: None, - }, + let canvas = self.canvases.get_mut(&keyframe_id).unwrap(); + + // Pre-fill dst from src so pixels outside this dab's bbox are preserved. + let mut copy_enc = device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { label: Some("canvas_copy_encoder") }, ); - pass.set_pipeline(&self.compute_pipeline); - pass.set_bind_group(0, &bg, &[]); - let wg_x = bbox_w.div_ceil(8); - let wg_y = bbox_h.div_ceil(8); - pass.dispatch_workgroups(wg_x, wg_y, 1); - } - queue.submit(Some(compute_encoder.finish())); + copy_enc.copy_texture_to_texture( + wgpu::TexelCopyTextureInfo { + texture: canvas.src(), + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyTextureInfo { + texture: canvas.dst(), + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + full_extent, + ); + queue.submit(Some(copy_enc.finish())); - // Swap: dst is now the authoritative source - canvas.swap(); + // Upload single-dab buffer and params + let dab_bytes = bytemuck::bytes_of(dab); + let dab_buf = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("dab_storage_buf"), + size: dab_bytes.len() as u64, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + queue.write_buffer(&dab_buf, 0, dab_bytes); + + let params = DabParams { + bbox_x0: x0 as i32, + bbox_y0: y0 as i32, + bbox_w, + bbox_h, + num_dabs: 1, + canvas_w, + canvas_h, + _pad: 0, + }; + let params_buf = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("dab_params_buf"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + queue.write_buffer(¶ms_buf, 0, bytemuck::bytes_of(¶ms)); + + let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("brush_dab_bg"), + layout: &self.compute_bg_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: dab_buf.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: params_buf.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView(canvas.src_view()), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::TextureView(canvas.dst_view()), + }, + ], + }); + + let mut compute_enc = device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { label: Some("brush_dab_encoder") }, + ); + { + let mut pass = compute_enc.begin_compute_pass( + &wgpu::ComputePassDescriptor { + label: Some("brush_dab_pass"), + timestamp_writes: None, + }, + ); + pass.set_pipeline(&self.compute_pipeline); + pass.set_bind_group(0, &bg, &[]); + pass.dispatch_workgroups(bbox_w.div_ceil(8), bbox_h.div_ceil(8), 1); + } + queue.submit(Some(compute_enc.finish())); + + // Swap: the just-written dst becomes src for the next dab. + eprintln!("[DAB] dispatched bbox=({},{},{},{}) current_before_swap={}", x0, y0, x1, y1, canvas.current); + canvas.swap(); + eprintln!("[DAB] after swap: current={}", canvas.current); + } } /// Read the current canvas back to a CPU `Vec` (raw RGBA, row-major). @@ -613,7 +623,7 @@ impl CanvasBlitPipeline { module: &shader, entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Rgba8Unorm, + format: wgpu::TextureFormat::Rgba16Float, blend: None, // canvas already stores premultiplied alpha write_mask: wgpu::ColorWrites::ALL, })], @@ -655,7 +665,7 @@ impl CanvasBlitPipeline { Self { pipeline, bg_layout, sampler, mask_sampler } } - /// Render the canvas texture into `target_view` (Rgba8Unorm) with the given camera. + /// Render the canvas texture into `target_view` (Rgba16Float) with the given camera. /// /// `target_view` is cleared to transparent before writing. /// `mask_view` is an R8Unorm texture in canvas-pixel space: 255 = keep, 0 = discard. diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/canvas_blit.wgsl b/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/canvas_blit.wgsl index 078b6e3..cf1084b 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/canvas_blit.wgsl +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/canvas_blit.wgsl @@ -1,8 +1,12 @@ // Canvas blit shader. // -// Renders a GPU raster canvas (at document resolution) into the layer's sRGB -// render buffer (at viewport resolution), applying the camera transform -// (pan + zoom) to map document-space pixels to viewport-space pixels. +// Renders a GPU raster canvas (at document resolution) into an Rgba16Float HDR +// buffer (at viewport resolution), applying the camera transform (pan + zoom) +// to map document-space pixels to viewport-space pixels. +// +// The canvas stores premultiplied linear RGBA. We output it as-is so the HDR +// compositor sees the same premultiplied-linear format it always works with, +// bypassing the sRGB intermediate used for Vello layers. // // Any viewport pixel whose corresponding document coordinate falls outside // [0, canvas_w) × [0, canvas_h) outputs transparent black. @@ -42,17 +46,6 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { return out; } -// Linear → sRGB encoding for a single channel. -// Applied to premultiplied linear values so the downstream srgb_to_linear -// pass round-trips correctly without darkening semi-transparent edges. -fn linear_to_srgb(c: f32) -> f32 { - return select( - 1.055 * pow(max(c, 0.0), 1.0 / 2.4) - 0.055, - c * 12.92, - c <= 0.0031308, - ); -} - @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { // Map viewport UV [0,1] → viewport pixel @@ -71,23 +64,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { } // The canvas stores premultiplied linear RGBA. - // The downstream pipeline (srgb_to_linear → compositor) expects the sRGB - // buffer to contain straight-alpha sRGB, i.e. the same format Vello outputs: - // sRGB buffer: srgb(r_straight), srgb(g_straight), srgb(b_straight), a - // srgb_to_linear: r_straight, g_straight, b_straight, a (linear straight) - // compositor: r_straight * a * opacity (premultiplied, correct) - // - // Without unpremultiplying, the compositor would double-premultiply: - // src = (premul_r, premul_g, premul_b, a) → output = premul_r * a = r * a² - // which produces a dark halo over transparent regions. + // The compositor expects straight-alpha linear (it premultiplies by src_alpha itself), + // so unpremultiply here. No sRGB conversion — the HDR buffer is linear throughout. let c = textureSample(canvas_tex, canvas_sampler, canvas_uv); let mask = textureSample(mask_tex, mask_sampler, canvas_uv).r; let masked_a = c.a * mask; let inv_a = select(0.0, 1.0 / c.a, c.a > 1e-6); - return vec4( - linear_to_srgb(c.r * inv_a), - linear_to_srgb(c.g * inv_a), - linear_to_srgb(c.b * inv_a), - masked_a, - ); + return vec4(c.r * inv_a, c.g * inv_a, c.b * inv_a, masked_a); } diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs index 84b8d12..dc0a76c 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs @@ -508,6 +508,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback { if let Some(ref float_sel) = self.ctx.selection.raster_floating { if let Ok(mut gpu_brush) = shared.gpu_brush.lock() { if !gpu_brush.canvases.contains_key(&float_sel.canvas_id) { + eprintln!("[CANVAS] lazy-init float canvas id={:?}", float_sel.canvas_id); gpu_brush.ensure_canvas(device, float_sel.canvas_id, float_sel.width, float_sel.height); if let Some(canvas) = gpu_brush.canvases.get(&float_sel.canvas_id) { let pixels = if float_sel.pixels.is_empty() { @@ -536,6 +537,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback { ); // On stroke start, upload the pre-stroke pixel data to both textures if let Some(ref pixels) = pending.initial_pixels { + eprintln!("[STROKE] uploading initial_pixels for kf={:?} painting_float={}", pending.keyframe_id, self.ctx.painting_float); if let Some(canvas) = gpu_brush.canvases.get(&pending.keyframe_id) { canvas.upload(queue, pixels); } @@ -592,6 +594,14 @@ impl egui_wgpu::CallbackTrait for VelloCallback { ); drop(image_cache); + // Debug frame counter (only active during strokes) + static FRAME: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + let dbg_frame = FRAME.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let dbg_stroke = self.ctx.painting_canvas.is_some(); + if dbg_stroke { + eprintln!("[FRAME {}] painting_canvas={:?} painting_float={}", dbg_frame, self.ctx.painting_canvas, self.ctx.painting_float); + } + // Get buffer pool for layer rendering let mut buffer_pool = shared.buffer_pool.lock().unwrap(); @@ -631,6 +641,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback { antialiasing_method: vello::AaConfig::Msaa16, }; + if dbg_stroke { eprintln!("[DRAW] background Vello render"); } if let Ok(mut renderer) = shared.renderer.lock() { renderer.render_to_texture(device, queue, &composite_result.background, bg_srgb_view, &bg_render_params).ok(); } @@ -650,6 +661,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback { // Clear to dark gray (stage background outside document bounds) // Note: stage_bg values are already in linear space for HDR compositing let stage_bg = [45.0 / 255.0, 45.0 / 255.0, 48.0 / 255.0, 1.0]; + if dbg_stroke { eprintln!("[COMPOSITE] background onto HDR"); } shared.compositor.composite( device, queue, @@ -757,12 +769,14 @@ impl egui_wgpu::CallbackTrait for VelloCallback { &instance_resources.hdr_texture_view, ) { // GPU canvas blit path: if a live GPU canvas exists for this - // raster layer, sample it directly instead of rendering the Vello - // scene (which lags until raw_pixels is updated after readback). + // raster layer, blit it directly into the HDR buffer (premultiplied + // linear → Rgba16Float), bypassing the sRGB intermediate entirely. + // Vello path: render to sRGB buffer → srgb_to_linear → HDR buffer. let used_gpu_canvas = if let Some(kf_id) = gpu_canvas_kf { let mut used = false; if let Ok(gpu_brush) = shared.gpu_brush.lock() { if let Some(canvas) = gpu_brush.canvases.get(&kf_id) { + if dbg_stroke { eprintln!("[DRAW] GPU canvas blit layer={:?} kf={:?} canvas.current={}", rendered_layer.layer_id, kf_id, canvas.current); } let camera = crate::gpu_brush::CameraParams { pan_x: self.ctx.pan_offset.x, pan_y: self.ctx.pan_offset.y, @@ -776,7 +790,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback { shared.canvas_blit.blit( device, queue, canvas.src_view(), - srgb_view, + hdr_layer_view, // blit directly to HDR &camera, None, // no mask on layer canvas blit ); @@ -789,19 +803,18 @@ impl egui_wgpu::CallbackTrait for VelloCallback { }; if !used_gpu_canvas { - // Render layer scene to sRGB buffer + // Render layer scene to sRGB buffer, then convert to HDR + if dbg_stroke { eprintln!("[DRAW] Vello render layer={:?} opacity={}", rendered_layer.layer_id, rendered_layer.opacity); } if let Ok(mut renderer) = shared.renderer.lock() { renderer.render_to_texture(device, queue, &rendered_layer.scene, srgb_view, &layer_render_params).ok(); } + let mut convert_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("layer_srgb_to_linear_encoder"), + }); + shared.srgb_to_linear.convert(device, &mut convert_encoder, srgb_view, hdr_layer_view); + queue.submit(Some(convert_encoder.finish())); } - // Convert sRGB to linear HDR - let mut convert_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("layer_srgb_to_linear_encoder"), - }); - shared.srgb_to_linear.convert(device, &mut convert_encoder, srgb_view, hdr_layer_view); - queue.submit(Some(convert_encoder.finish())); - // Composite this layer onto the HDR accumulator with its opacity let compositor_layer = lightningbeam_core::gpu::CompositorLayer::new( hdr_layer_handle, @@ -809,6 +822,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback { rendered_layer.blend_mode, ); + if dbg_stroke { eprintln!("[COMPOSITE] layer={:?} opacity={} blend={:?} used_gpu_canvas={}", rendered_layer.layer_id, rendered_layer.opacity, rendered_layer.blend_mode, used_gpu_canvas); } let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("layer_composite_encoder"), }); @@ -1003,6 +1017,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback { // Blit the float GPU canvas on top of all composited layers. // The float_mask_view clips to the selection shape (None = full float visible). + if dbg_stroke { eprintln!("[FRAME {}] float blit section: raster_floating={}", dbg_frame, self.ctx.selection.raster_floating.is_some()); } if let Some(ref float_sel) = self.ctx.selection.raster_floating { let float_canvas_id = float_sel.canvas_id; let float_x = float_sel.x; @@ -1011,10 +1026,9 @@ impl egui_wgpu::CallbackTrait for VelloCallback { let float_h = float_sel.height; if let Ok(gpu_brush) = shared.gpu_brush.lock() { if let Some(canvas) = gpu_brush.canvases.get(&float_canvas_id) { - let float_srgb_handle = buffer_pool.acquire(device, layer_spec); - let float_hdr_handle = buffer_pool.acquire(device, hdr_spec); - if let (Some(fsrgb_view), Some(fhdr_view), Some(hdr_view)) = ( - buffer_pool.get_view(float_srgb_handle), + if dbg_stroke { eprintln!("[DRAW] float canvas blit canvas_id={:?} canvas.current={}", float_canvas_id, canvas.current); } + let float_hdr_handle = buffer_pool.acquire(device, hdr_spec); + if let (Some(fhdr_view), Some(hdr_view)) = ( buffer_pool.get_view(float_hdr_handle), &instance_resources.hdr_texture_view, ) { @@ -1028,27 +1042,22 @@ impl egui_wgpu::CallbackTrait for VelloCallback { viewport_h: height as f32, _pad: 0.0, }; + // Blit directly to HDR (straight-alpha linear, no sRGB step) shared.canvas_blit.blit( device, queue, canvas.src_view(), - fsrgb_view, + fhdr_view, &fcamera, float_mask_view.as_ref(), ); - let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("float_canvas_srgb_to_linear"), - }); - shared.srgb_to_linear.convert(device, &mut enc, fsrgb_view, fhdr_view); - queue.submit(Some(enc.finish())); - let float_layer = lightningbeam_core::gpu::CompositorLayer::normal(float_hdr_handle, 1.0); + if dbg_stroke { eprintln!("[COMPOSITE] float canvas onto HDR"); } let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("float_canvas_composite"), }); shared.compositor.composite(device, queue, &mut enc, &[float_layer], &buffer_pool, hdr_view, None); queue.submit(Some(enc.finish())); } - buffer_pool.release(float_srgb_handle); buffer_pool.release(float_hdr_handle); } } @@ -2180,6 +2189,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback { antialiasing_method: vello::AaConfig::Msaa16, }; + if self.ctx.painting_canvas.is_some() { eprintln!("[DRAW] overlay Vello render"); } if let Ok(mut renderer) = shared.renderer.lock() { renderer.render_to_texture(device, queue, &scene, overlay_srgb_view, &overlay_params).ok(); } @@ -2193,6 +2203,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback { // Composite overlay onto HDR texture let overlay_layer = lightningbeam_core::gpu::CompositorLayer::normal(overlay_hdr_handle, 1.0); + if self.ctx.painting_canvas.is_some() { eprintln!("[COMPOSITE] overlay onto HDR"); } let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("overlay_composite_encoder"), });