diff --git a/frontends/rioterm/src/application.rs b/frontends/rioterm/src/application.rs index 9e055adcb4..163f67c666 100644 --- a/frontends/rioterm/src/application.rs +++ b/frontends/rioterm/src/application.rs @@ -14,6 +14,7 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use raw_window_handle::HasDisplayHandle; use rio_backend::clipboard::{Clipboard, ClipboardType}; use rio_backend::config::colors::{ColorRgb, NamedColor}; +use rio_backend::config::theme::AppearanceTheme; use rio_window::application::ApplicationHandler; use rio_window::event::{ ElementState, Ime, MouseButton, MouseScrollDelta, StartCause, TouchPhase, WindowEvent, @@ -395,17 +396,19 @@ impl ApplicationHandler for Application<'_> { self.config = config; + let color_scheme = self.config.force_theme.or_else(|| { + event_loop + .system_theme() + .map(AppearanceTheme::from_window_theme) + }); let mut has_checked_adaptive_colors = false; for (_id, route) in self.router.routes.iter_mut() { // Apply system theme to ensure colors are consistent if !has_checked_adaptive_colors { - let system_theme = event_loop.system_theme(); - let theme = self - .config - .force_theme - .map(|t| t.to_window_theme()) - .or(system_theme); - update_colors_based_on_theme(&mut self.config, theme); + update_colors_based_on_theme( + &mut self.config, + color_scheme.map(AppearanceTheme::to_window_theme), + ); has_checked_adaptive_colors = true; } @@ -425,6 +428,7 @@ impl ApplicationHandler for Application<'_> { &self.config, &self.router.font_library, has_font_updates, + color_scheme, ); route.window.configure_window(&self.config); @@ -858,7 +862,6 @@ impl ApplicationHandler for Application<'_> { } RioEventType::Rio(RioEvent::ToggleAppearanceTheme) => { if let Some(route) = self.router.routes.get_mut(&window_id) { - use rio_backend::config::theme::AppearanceTheme; let current = self .config .force_theme @@ -880,6 +883,7 @@ impl ApplicationHandler for Application<'_> { &self.config, &self.router.font_library, false, + Some(toggled), ); route.window.configure_window(&self.config); } @@ -1185,7 +1189,13 @@ impl ApplicationHandler for Application<'_> { // targets a different panel, switch to it regardless // of mouse mode (e.g. neovim capturing clicks). if route.window.screen.select_current_based_on_mouse() { + // Focus-switch click: clear the now-focused pane's + // stale selection and reset click_state so the + // in-flight press can't drag-extend the old anchor. + route.window.screen.clear_selection(); + route.window.screen.mouse.click_state = ClickState::Click; route.request_redraw(); + return; } else if !route.window.screen.modifiers.state().shift_key() && route.window.screen.mouse_mode() { @@ -1827,6 +1837,7 @@ impl ApplicationHandler for Application<'_> { &self.config, &self.router.font_library, false, + Some(AppearanceTheme::from_window_theme(new_theme)), ); route.window.configure_window(&self.config); route.request_redraw(); diff --git a/frontends/rioterm/src/context/mod.rs b/frontends/rioterm/src/context/mod.rs index a7d7c9afca..a25f151559 100644 --- a/frontends/rioterm/src/context/mod.rs +++ b/frontends/rioterm/src/context/mod.rs @@ -134,6 +134,9 @@ pub struct ContextManagerConfig { pub title: rio_backend::config::title::Title, pub keyboard: rio_backend::config::keyboard::Keyboard, pub scrollback_history_limit: usize, + /// Initial color-scheme polarity for new panes, so the + /// `CSI ? 996 n` query answers correctly before any theme change. + pub color_scheme_is_dark: bool, } const DEFAULT_CONTEXT_CAPACITY: usize = 28; @@ -245,6 +248,7 @@ impl ContextManager { config.scrollback_history_limit, ); terminal.blinking_cursor = cursor_state.1; + terminal.set_color_scheme(config.color_scheme_is_dark); let terminal: Arc>> = Arc::new(FairMutex::new(terminal)); let pty; @@ -1065,6 +1069,7 @@ impl ContextManager { title: config.title, keyboard: config.keyboard, scrollback_history_limit: config.scrollback_history_limit, + color_scheme_is_dark: self.config.color_scheme_is_dark, }; let current = self.current(); diff --git a/frontends/rioterm/src/grid_emit.rs b/frontends/rioterm/src/grid_emit.rs index b9c16e4cff..7cea42a7b4 100644 --- a/frontends/rioterm/src/grid_emit.rs +++ b/frontends/rioterm/src/grid_emit.rs @@ -1264,6 +1264,23 @@ impl GridGlyphRasterizer { } } + /// Drop every `font_id`-keyed cache. Called on a font-library + /// swap: the new library reuses the same `font_id` slots, so + /// stale entries would otherwise feed shaping (advances) and + /// `ascent_px` (vertical placement) from the old font. + pub fn reset_font_caches(&mut self) { + self.font_resolve.clear(); + self.ascent_cache.clear(); + self.synthesis_cache.clear(); + for bucket in &mut self.run_cache { + bucket.clear(); + } + #[cfg(target_os = "macos")] + self.handle_cache.clear(); + #[cfg(not(target_os = "macos"))] + self.font_data_cache.clear(); + } + #[inline] fn resolve_font( &mut self, diff --git a/frontends/rioterm/src/router/mod.rs b/frontends/rioterm/src/router/mod.rs index 170a8d097b..4534eeb680 100644 --- a/frontends/rioterm/src/router/mod.rs +++ b/frontends/rioterm/src/router/mod.rs @@ -9,6 +9,7 @@ use crate::screen::{Screen, ScreenWindowProperties}; use assistant::Assistant; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use rio_backend::clipboard::Clipboard; +use rio_backend::config::theme::AppearanceTheme; use rio_backend::config::Config as RioConfig; use rio_backend::error::{RioError, RioErrorLevel, RioErrorType}; @@ -89,10 +90,11 @@ impl Route<'_> { config: &RioConfig, db: &rio_backend::sugarloaf::font::FontLibrary, should_update_font: bool, + color_scheme: Option, ) { self.window .screen - .update_config(config, db, should_update_font); + .update_config(config, db, should_update_font, color_scheme); } #[inline] @@ -703,8 +705,18 @@ impl<'a> RouteWindow<'a> { window_id: winit_window.id(), }; - let screen = Screen::new(properties, config, event_proxy, font_library, open_url) - .expect("Screen not created"); + let color_scheme = config + .force_theme + .or_else(|| winit_window.theme().map(AppearanceTheme::from_window_theme)); + let screen = Screen::new( + properties, + config, + event_proxy, + font_library, + open_url, + color_scheme, + ) + .expect("Screen not created"); if config.window.columns.is_some() || config.window.rows.is_some() { let (physical_width, physical_height) = compute_window_size_from_grid( diff --git a/frontends/rioterm/src/screen/mod.rs b/frontends/rioterm/src/screen/mod.rs index 022f91c679..61fdba3ecb 100644 --- a/frontends/rioterm/src/screen/mod.rs +++ b/frontends/rioterm/src/screen/mod.rs @@ -37,6 +37,7 @@ use rio_backend::clipboard::Clipboard; use rio_backend::clipboard::ClipboardType; use rio_backend::config::layout::Margin; use rio_backend::config::renderer::Backend; +use rio_backend::config::theme::AppearanceTheme; use rio_backend::crosswords::pos::{Boundary, CursorState, Direction, Line}; use rio_backend::crosswords::search::RegexSearch; use rio_backend::error::{RioError, RioErrorLevel, RioErrorType}; @@ -119,6 +120,7 @@ impl Screen<'_> { event_proxy: EventProxy, font_library: &rio_backend::sugarloaf::font::FontLibrary, open_url: Option, + color_scheme: Option, ) -> Result, Box> { let size = window_properties.size; let scale = window_properties.scale; @@ -228,6 +230,7 @@ impl Screen<'_> { title: config.title.clone(), keyboard: config.keyboard, scrollback_history_limit: config.scrollback_history_limit, + color_scheme_is_dark: !matches!(color_scheme, Some(AppearanceTheme::Light)), }; let rich_text_id = next_rich_text_id(); @@ -412,6 +415,7 @@ impl Screen<'_> { config: &rio_backend::config::Config, font_library: &rio_backend::sugarloaf::font::FontLibrary, should_update_font_library: bool, + color_scheme: Option, ) { let num_tabs = self.ctx().len(); let padding_y_top = padding_top_from_config( @@ -422,8 +426,19 @@ impl Screen<'_> { ); let padding_y_bottom = config.margin.bottom; + // Resolved OS/adaptive scheme drives the DECSET 2031 notify. + let scheme_is_dark = !matches!(color_scheme, Some(AppearanceTheme::Light)); + let color_scheme_changed = + self.context_manager.config.color_scheme_is_dark != scheme_is_dark; + if should_update_font_library { self.sugarloaf.update_font(font_library); + // New library reuses the same font_ids, so font_id-keyed + // caches must be dropped or they serve old-font data. + self.grid_rasterizer.reset_font_caches(); + for grid in self.grids.values_mut() { + grid.clear_glyph_cache(); + } } let s = self.sugarloaf.style_mut(); s.font_size = config.fonts.size; @@ -474,10 +489,20 @@ impl Screen<'_> { let mut terminal = current_context.terminal.lock(); current_context.renderable_content = RenderableContent::from_cursor_config(&config.cursor); + // Idle panes are skipped by the render loop, so force a + // full repaint or they keep the old theme. + current_context + .renderable_content + .pending_update + .set_terminal_damage(rio_backend::event::TerminalDamage::Full); let shape = config.cursor.shape; terminal.cursor_shape = shape; terminal.default_cursor_shape = shape; terminal.blinking_cursor = config.cursor.blinking; + terminal.set_color_scheme(scheme_is_dark); + if color_scheme_changed { + terminal.report_color_scheme(scheme_is_dark); + } drop(terminal); } } @@ -487,6 +512,9 @@ impl Screen<'_> { // Update keyboard config in context manager self.context_manager.config.keyboard = config.keyboard; + // Panes spawned after a theme change must start with the new + // scheme so their `CSI ? 996 n` query answers correctly. + self.context_manager.config.color_scheme_is_dark = scheme_is_dark; // Re-evaluate the opaque flag — toggling `window.opacity` / // `window.blur` at runtime should flip the compositor mode. diff --git a/rio-backend/src/ansi/mode.rs b/rio-backend/src/ansi/mode.rs index e79f3f42e5..f42888628b 100644 --- a/rio-backend/src/ansi/mode.rs +++ b/rio-backend/src/ansi/mode.rs @@ -69,6 +69,7 @@ impl PrivateMode { 1049 => Self::Named(NamedPrivateMode::SwapScreenAndSetRestoreCursor), 2004 => Self::Named(NamedPrivateMode::BracketedPaste), 2026 => Self::Named(NamedPrivateMode::SyncUpdate), + 2031 => Self::Named(NamedPrivateMode::ColorSchemeUpdates), _ => Self::Unknown(mode), } } @@ -120,6 +121,9 @@ pub enum NamedPrivateMode { BracketedPaste = 2004, /// The mode is handled automatically by [`Processor`]. SyncUpdate = 2026, + /// App opts in to unsolicited DSR `CSI ? 997 n` light/dark + /// color-scheme notifications (DECSET 2031). + ColorSchemeUpdates = 2031, } /// Mode for clearing line. diff --git a/rio-backend/src/crosswords/mod.rs b/rio-backend/src/crosswords/mod.rs index c5d734253c..ddec46ac5a 100644 --- a/rio-backend/src/crosswords/mod.rs +++ b/rio-backend/src/crosswords/mod.rs @@ -98,6 +98,7 @@ bitflags! { const REPORT_ALTERNATE_KEYS = 1 << 20; const REPORT_ALL_KEYS_AS_ESC = 1 << 21; const REPORT_ASSOCIATED_TEXT = 1 << 22; + const COLOR_SCHEME_UPDATES = 1 << 23; const MOUSE_MODE = Self::MOUSE_REPORT_CLICK.bits() | Self::MOUSE_MOTION.bits() | Self::MOUSE_DRAG.bits(); const KITTY_KEYBOARD_PROTOCOL = Self::DISAMBIGUATE_ESC_CODES.bits() | Self::REPORT_EVENT_TYPES.bits() @@ -464,6 +465,10 @@ where keyboard_mode_idx: usize, inactive_keyboard_mode_stack: [u8; KEYBOARD_MODE_STACK_MAX_DEPTH], inactive_keyboard_mode_idx: usize, + + /// Last-known color scheme polarity, kept current by the frontend on + /// every config update. Answers the DSR `CSI ? 996 n` query. + color_scheme_is_dark: bool, } impl Crosswords { @@ -518,6 +523,7 @@ impl Crosswords { keyboard_mode_idx: 0, inactive_keyboard_mode_stack: Default::default(), inactive_keyboard_mode_idx: 0, + color_scheme_is_dark: true, } } @@ -1512,6 +1518,10 @@ impl Crosswords { // Reset alternate screen contents. self.inactive_grid.reset_region(..); + } else { + // Leaving alt screen: the full-screen app that set any OSC + // color overrides is exiting, so drop them. + self.reset_all_colors(); } mem::swap( @@ -1535,12 +1545,58 @@ impl Crosswords { self.mark_fully_damaged(); } + /// Drop every per-pane OSC color override. The bg reset is pushed + /// to the frontend so the window background (derived from OSC 11) + /// follows. Caller is responsible for damage. + fn reset_all_colors(&mut self) { + let bg = NamedColor::Background as usize; + let had_bg_override = self.colors[bg].is_some(); + + self.colors = TermColors::default(); + + if had_bg_override { + self.event_proxy.send_event( + RioEvent::ColorChange(self.route_id, bg, None), + self.window_id, + ); + } + } + #[inline] pub fn mark_line_damaged(&mut self, line: Line) { let line_idx = line.0 as usize; self.damage.damage_line(line_idx); } + /// Record the current color scheme so a later `CSI ? 996 n` query + /// gets the right answer. Does not notify the app. + pub fn set_color_scheme(&mut self, is_dark: bool) { + self.color_scheme_is_dark = is_dark; + } + + /// If an app subscribed via DECSET 2031, push an unsolicited + /// scheme-change DSR so it re-queries colors without a restart. + pub fn report_color_scheme(&self, is_dark: bool) { + if !self.mode.contains(Mode::COLOR_SCHEME_UPDATES) { + return; + } + self.send_color_scheme_dsr(is_dark); + } + + /// Reply to the DSR query `CSI ? 996 n` with the current scheme. + /// Not gated by DECSET 2031 — it's a direct request. + fn reply_color_scheme_query(&self) { + self.send_color_scheme_dsr(self.color_scheme_is_dark); + } + + fn send_color_scheme_dsr(&self, is_dark: bool) { + let polarity = if is_dark { 1 } else { 2 }; + self.event_proxy.send_event( + RioEvent::PtyWrite(self.route_id, format!("\x1b[?997;{polarity}n")), + self.window_id, + ); + } + pub fn selection_to_string(&self) -> Option { let selection_range = self.selection.as_ref().and_then(|s| s.to_range(self))?; let SelectionRange { start, end, .. } = selection_range; @@ -2013,6 +2069,9 @@ impl Handler for Crosswords { self.event_proxy .send_event(RioEvent::CursorBlinkingChange, self.window_id); } + NamedPrivateMode::ColorSchemeUpdates => { + self.mode.insert(Mode::COLOR_SCHEME_UPDATES) + } NamedPrivateMode::SyncUpdate => (), } } @@ -2085,6 +2144,9 @@ impl Handler for Crosswords { self.event_proxy .send_event(RioEvent::CursorBlinkingChange, self.window_id); } + NamedPrivateMode::ColorSchemeUpdates => { + self.mode.remove(Mode::COLOR_SCHEME_UPDATES) + } NamedPrivateMode::SyncUpdate => (), } } @@ -2131,6 +2193,9 @@ impl Handler for Crosswords { NamedPrivateMode::BracketedPaste => { self.mode.contains(Mode::BRACKETED_PASTE).into() } + NamedPrivateMode::ColorSchemeUpdates => { + self.mode.contains(Mode::COLOR_SCHEME_UPDATES).into() + } NamedPrivateMode::SyncUpdate => ModeState::Reset, NamedPrivateMode::ColumnMode => ModeState::NotSupported, }, @@ -2969,6 +3034,11 @@ impl Handler for Crosswords { }; } + #[inline] + fn report_color_scheme_query(&mut self) { + self.reply_color_scheme_query(); + } + #[inline] fn newline(&mut self) { self.linefeed(); @@ -6685,4 +6755,71 @@ mod tests { // Cursor must be untouched. assert_eq!(cw.grid.cursor.pos, cursor_before); } + + #[test] + fn test_color_scheme_notification() { + use crate::performer::handler::Handler; + use std::cell::RefCell; + use std::rc::Rc; + + #[derive(Clone)] + struct TestListener { + events: Rc>>, + } + + impl EventListener for TestListener { + fn event(&self) -> (Option, bool) { + (None, false) + } + + fn send_event(&self, event: RioEvent, _id: WindowId) { + self.events.borrow_mut().push(event); + } + } + + let size = CrosswordsSize::new(10, 10); + let window_id = WindowId::from(0); + let events = Rc::new(RefCell::new(Vec::new())); + let listener = TestListener { + events: events.clone(), + }; + let mut term = + Crosswords::new(size, CursorShape::Block, listener, window_id, 0, 10_000); + + let collect_writes = |events: &Rc>>| { + let writes: Vec = events + .borrow() + .iter() + .filter_map(|e| match e { + RioEvent::PtyWrite(_, text) => Some(text.clone()), + _ => None, + }) + .collect(); + events.borrow_mut().clear(); + writes + }; + + // Not subscribed: no unsolicited notification. + term.report_color_scheme(true); + assert!(events.borrow().is_empty(), "no event without DECSET 2031"); + + // Subscribe via DECSET 2031, then notify dark, then light. + term.set_private_mode(NamedPrivateMode::ColorSchemeUpdates.into()); + term.report_color_scheme(true); + term.report_color_scheme(false); + assert_eq!( + collect_writes(&events), + vec!["\x1b[?997;1n", "\x1b[?997;2n"] + ); + + // DSR query `CSI ? 996 n` replies with the stored scheme, + // regardless of DECSET 2031. + term.unset_private_mode(NamedPrivateMode::ColorSchemeUpdates.into()); + term.set_color_scheme(false); + term.report_color_scheme_query(); + assert_eq!(collect_writes(&events), vec!["\x1b[?997;2n"]); + term.set_color_scheme(true); + term.report_color_scheme_query(); + assert_eq!(collect_writes(&events), vec!["\x1b[?997;1n"]); + } } diff --git a/rio-backend/src/performer/handler.rs b/rio-backend/src/performer/handler.rs index 36513ad157..472aac02c6 100644 --- a/rio-backend/src/performer/handler.rs +++ b/rio-backend/src/performer/handler.rs @@ -151,6 +151,9 @@ pub trait Handler { /// Report device status. fn device_status(&mut self, _: usize) {} + /// Reply to the color-scheme query DSR (`CSI ? 996 n`). + fn report_color_scheme_query(&mut self) {} + /// Move cursor forward `cols`. fn move_forward(&mut self, _: Column) {} @@ -1317,6 +1320,14 @@ impl Perform for Performer<'_, U> { } } ('n', []) => handler.device_status(next_param_or(0) as usize), + ('n', [b'?']) => { + // DSR for private params. 996 = query current color scheme. + if next_param_or(0) == 996 { + handler.report_color_scheme_query(); + } else { + csi_unhandled!(); + } + } ('P', []) => handler.delete_chars(next_param_or(1) as usize), ('p', [b'$']) => { let mode = next_param_or(0); diff --git a/sugarloaf/src/grid/cpu.rs b/sugarloaf/src/grid/cpu.rs index 205544d381..eb6b9d7541 100644 --- a/sugarloaf/src/grid/cpu.rs +++ b/sugarloaf/src/grid/cpu.rs @@ -76,6 +76,14 @@ impl CpuGridAtlas { self.slots.get(&key).copied() } + /// Drop all cached glyphs and free the packing space. Stale + /// pixels are left in `pixels` — harmless, since every slot is + /// overwritten when its region is reallocated. + pub fn clear(&mut self) { + self.allocator.clear(); + self.slots.clear(); + } + pub fn insert( &mut self, key: GlyphKey, @@ -301,6 +309,13 @@ impl CpuGridRenderer { } } + /// Drop both atlases' cached glyphs. See [`GridRenderer::clear_glyph_cache`]. + pub fn clear_glyph_cache(&mut self) { + self.atlas_grayscale.clear(); + self.atlas_color.clear(); + self.needs_full_rebuild = true; + } + #[inline] pub fn needs_full_rebuild(&self) -> bool { self.needs_full_rebuild diff --git a/sugarloaf/src/grid/metal.rs b/sugarloaf/src/grid/metal.rs index e193722b6c..4d54e48584 100644 --- a/sugarloaf/src/grid/metal.rs +++ b/sugarloaf/src/grid/metal.rs @@ -481,6 +481,14 @@ impl MetalGridRenderer { } } + /// Drop both atlases' cached glyphs. See [`GridRenderer::clear_glyph_cache`]. + pub fn clear_glyph_cache(&mut self) { + self.atlas_grayscale.clear(); + if let Some(atlas) = &mut self.atlas_color { + atlas.clear(); + } + } + pub fn resize(&mut self, cols: u32, rows: u32) { if cols == self.cols && rows == self.rows { return; diff --git a/sugarloaf/src/grid/mod.rs b/sugarloaf/src/grid/mod.rs index 894cf59775..0abfe8ed7b 100644 --- a/sugarloaf/src/grid/mod.rs +++ b/sugarloaf/src/grid/mod.rs @@ -347,6 +347,22 @@ impl GridRenderer { } } + /// Drop every cached glyph from both atlases. Call on a font swap: + /// the glyph cache is keyed by `(font_id, glyph_id, size_bucket)` + /// and the new font library reuses the same `font_id` slots, so + /// stale entries would serve old-font bitmaps for reused glyph_ids. + pub fn clear_glyph_cache(&mut self) { + match self { + #[cfg(target_os = "macos")] + GridRenderer::Metal(r) => r.clear_glyph_cache(), + #[cfg(feature = "wgpu")] + GridRenderer::Wgpu(r) => r.clear_glyph_cache(), + #[cfg(target_os = "linux")] + GridRenderer::Vulkan(r) => r.clear_glyph_cache(), + GridRenderer::Cpu(r) => r.clear_glyph_cache(), + } + } + /// `true` on the first frame after `new` or `resize`. Callers /// should treat this as "force full rebuild regardless of /// per-row damage" since the underlying CPU buffers are zeroed. diff --git a/sugarloaf/src/grid/vulkan.rs b/sugarloaf/src/grid/vulkan.rs index d5834ed477..c57a8c1478 100644 --- a/sugarloaf/src/grid/vulkan.rs +++ b/sugarloaf/src/grid/vulkan.rs @@ -279,6 +279,17 @@ impl VulkanGlyphAtlas { self.slots.get(&key).copied() } + /// Drop all cached glyphs and free the packing space in every + /// page. The GPU page textures stay allocated — their texels are + /// overwritten as new glyphs repack from the reset allocators. + /// Called on a font swap, where `GlyphKey`'s `font_id` is reused. + pub fn clear(&mut self) { + for page in &mut self.pages { + page.allocator.clear(); + } + self.slots.clear(); + } + /// Descriptor set for the given page index. Bound to set=1 by /// `render_text` for each `(kind, page)` bucket of cells. #[inline] @@ -921,6 +932,12 @@ impl VulkanGridRenderer { self.atlas_color.insert(key, glyph) } + /// Drop both atlases' cached glyphs. See [`GridRenderer::clear_glyph_cache`]. + pub fn clear_glyph_cache(&mut self) { + self.atlas_grayscale.clear(); + self.atlas_color.clear(); + } + /// Drain pending atlas uploads into `cmd`. MUST be called BEFORE /// `Sugarloaf::render_vulkan` opens its dynamic-rendering pass — /// `vkCmdCopyBufferToImage` is forbidden inside a render pass. diff --git a/sugarloaf/src/grid/webgpu.rs b/sugarloaf/src/grid/webgpu.rs index 21b7b7c838..086ea550b5 100644 --- a/sugarloaf/src/grid/webgpu.rs +++ b/sugarloaf/src/grid/webgpu.rs @@ -421,6 +421,12 @@ impl WgpuGridRenderer { self.atlas_color.insert(key, glyph) } + /// Drop both atlases' cached glyphs. See [`GridRenderer::clear_glyph_cache`]. + pub fn clear_glyph_cache(&mut self) { + self.atlas_grayscale.clear(); + self.atlas_color.clear(); + } + /// Record the cell-bg pass. Uploads the uniform buffer (cheap; the /// bg path always runs first per frame so this is the right place /// for it) and the bg cell storage buffer if it changed since the