tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit d074dc674d9df7759c639fc78b30cf88fada6710
parent d6a24ccccb108abf4ff49730b26441f6ba321c2d
Author: Nicolas Silva <nical@fastmail.com>
Date:   Thu, 23 Oct 2025 20:34:36 +0000

Bug 1995958 - Add a rudimentary texture viewer to the WR debugger. r=gw

This adds the 4 categories of textures to the documents panel with a button to fetch textures per category.
The textures can then be selected and previewed with a very rudimentary ui in the viewer panel.

It also adds the following commands: get-textures, get-atlas-textures, get-standalone-textures, get-target-textures and get-tile-textures.
When used from the cli, the textures are saved as pngs in the current folder.

The texture viewer ui is too basic to be really useful. I would like it to provide ways to zoom, color pick and save the textures at the very least, but that's a start.

Differential Revision: https://phabricator.services.mozilla.com/D269735

Diffstat:
Mgfx/wr/webrender/Cargo.toml | 2+-
Mgfx/wr/webrender/src/debugger.rs | 10++++++++--
Mgfx/wr/webrender/src/internal_types.rs | 13+------------
Mgfx/wr/webrender/src/picture_textures.rs | 6+++---
Mgfx/wr/webrender/src/render_backend.rs | 3++-
Mgfx/wr/webrender/src/renderer/mod.rs | 47++++++++++++++++++++++++++++++++++++++++++++---
Mgfx/wr/webrender/src/texture_cache.rs | 4++--
Mgfx/wr/webrender_api/Cargo.toml | 2+-
Mgfx/wr/webrender_api/src/debugger.rs | 11+++++++++++
Mgfx/wr/webrender_api/src/lib.rs | 11+++++++++++
Mgfx/wr/wrshell/Cargo.lock | 44+++++++++++++++++++++++++++++++++++++++++---
Mgfx/wr/wrshell/Cargo.toml | 1+
Mgfx/wr/wrshell/src/cli.rs | 20++++++++++++++++++++
Mgfx/wr/wrshell/src/command.rs | 2++
Mgfx/wr/wrshell/src/debug_commands.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mgfx/wr/wrshell/src/gui/mod.rs | 54++++++++++++++++++++++++++++++++++++++++++------------
Agfx/wr/wrshell/src/gui/textures.rs | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
17 files changed, 425 insertions(+), 40 deletions(-)

diff --git a/gfx/wr/webrender/Cargo.toml b/gfx/wr/webrender/Cargo.toml @@ -20,7 +20,7 @@ static_freetype = ["glyph_rasterizer/static_freetype"] leak_checks = [] gecko = ["firefox-on-glean", "glean", "glyph_rasterizer/gecko"] sw_compositor = ["swgl"] -debugger = ["hyper", "tokio", "serde_json", "url", "sha1", "base64", "api/debugger"] +debugger = ["hyper", "tokio", "serde_json", "url", "sha1", "base64", "api/debugger", "capture"] [build-dependencies] build-parallel = "0.1.2" diff --git a/gfx/wr/webrender/src/debugger.rs b/gfx/wr/webrender/src/debugger.rs @@ -9,7 +9,7 @@ use std::collections::HashMap; use std::convert::Infallible; use api::crossbeam_channel; use api::channel::{Sender, unbounded_channel}; -use api::DebugFlags; +use api::{DebugFlags, TextureCacheCategory}; use api::debugger::{DebuggerMessage, SetDebugFlagsMessage, ProfileCounterDescriptor}; use api::debugger::{UpdateProfileCountersMessage, InitProfileCountersMessage, ProfileCounterId}; use api::debugger::{CompositorDebugInfo, CompositorDebugTile}; @@ -62,6 +62,8 @@ pub enum DebugQueryKind { CompositorConfig {}, /// Query the compositing view CompositorView {}, + /// Query the content of GPU textures + Textures { category: Option<TextureCacheCategory> }, } /// Details about the debug query being requested @@ -268,11 +270,15 @@ async fn handle_request( "/query" => { // Query internal state about WR. let (tx, rx) = crossbeam_channel::unbounded(); - let kind = match args.get("type").map(|s| s.as_str()) { Some("spatial-tree") => DebugQueryKind::SpatialTree {}, Some("composite-view") => DebugQueryKind::CompositorView {}, Some("composite-config") => DebugQueryKind::CompositorConfig {}, + Some("textures") => DebugQueryKind::Textures { category: None }, + Some("atlas-textures") => DebugQueryKind::Textures { category: Some(TextureCacheCategory::Atlas) }, + Some("target-textures") => DebugQueryKind::Textures { category: Some(TextureCacheCategory::RenderTarget) }, + Some("tile-textures") => DebugQueryKind::Textures { category: Some(TextureCacheCategory::PictureTile) }, + Some("standalone-textures") => DebugQueryKind::Textures { category: Some(TextureCacheCategory::Standalone) }, _ => { return Ok(string_response("Unknown query")); } diff --git a/gfx/wr/webrender/src/internal_types.rs b/gfx/wr/webrender/src/internal_types.rs @@ -4,7 +4,7 @@ use api::{ColorF, DocumentId, ExternalImageId, PrimitiveFlags, Parameter, RenderReasons}; use api::{ImageFormat, NotificationRequest, Shadow, FilterOpGraphPictureBufferId, FilterOpGraphPictureReference, FilterOpGraphNode, FilterOp, ImageBufferKind}; -use api::FramePublishId; +use api::{FramePublishId, TextureCacheCategory}; use api::units::*; use crate::render_api::DebugCommand; use crate::composite::NativeSurfaceOperation; @@ -1113,17 +1113,6 @@ pub struct TextureCacheAllocation { pub kind: TextureCacheAllocationKind, } -/// A little bit of extra information to make memory reports more useful -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub enum TextureCacheCategory { - Atlas, - Standalone, - PictureTile, - RenderTarget, -} - /// Information used when allocating / reallocating. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct TextureCacheAllocInfo { diff --git a/gfx/wr/webrender/src/picture_textures.rs b/gfx/wr/webrender/src/picture_textures.rs @@ -4,11 +4,11 @@ use std::mem; use smallvec::SmallVec; -use api::{ImageFormat, ImageBufferKind, DebugFlags}; +use api::{ImageFormat, ImageBufferKind, DebugFlags, TextureCacheCategory}; use api::units::*; use crate::device::TextureFilter; use crate::internal_types::{ - CacheTextureId, TextureUpdateList, Swizzle, TextureCacheAllocInfo, TextureCacheCategory, + CacheTextureId, TextureUpdateList, Swizzle, TextureCacheAllocInfo, TextureSource, FrameStamp, FrameId, }; use crate::profiler::{self, TransactionProfile}; @@ -229,7 +229,7 @@ impl PictureTextures { self.cache_handles.push(strong_handle); - new_handle + new_handle } pub fn free_tile( diff --git a/gfx/wr/webrender/src/render_backend.rs b/gfx/wr/webrender/src/render_backend.rs @@ -1237,7 +1237,8 @@ impl RenderBackend { return RenderBackendStatus::Continue; } DebugQueryKind::CompositorView { .. } | - DebugQueryKind::CompositorConfig { .. } => { + DebugQueryKind::CompositorConfig { .. } | + DebugQueryKind::Textures { .. } => { ResultMsg::DebugCommand(option) } } diff --git a/gfx/wr/webrender/src/renderer/mod.rs b/gfx/wr/webrender/src/renderer/mod.rs @@ -34,7 +34,7 @@ //! up the scissor, are accepting already transformed coordinates, which we can get by //! calling `DrawTarget::to_framebuffer_rect` -use api::{ClipMode, ColorF, ColorU, MixBlendMode}; +use api::{ClipMode, ColorF, ColorU, MixBlendMode, TextureCacheCategory}; use api::{DocumentId, Epoch, ExternalImageHandler, RenderReasons}; #[cfg(feature = "replay")] use api::ExternalImageId; @@ -59,7 +59,7 @@ use crate::composite::{CompositorKind, Compositor, NativeTileId, CompositeFeatur use crate::composite::{CompositorConfig, NativeSurfaceOperationDetails, NativeSurfaceId, NativeSurfaceOperation, ClipRadius}; use crate::composite::TileKind; #[cfg(feature = "debugger")] -use api::debugger::CompositorDebugInfo; +use api::debugger::{CompositorDebugInfo, DebuggerTextureContent}; use crate::segment::SegmentBuilder; use crate::{debug_colors, CompositorInputConfig, CompositorSurfaceUsage}; use crate::device::{DepthFunction, Device, DrawTarget, ExternalTexture, GpuFrameId, UploadPBOPool}; @@ -74,7 +74,7 @@ use crate::gpu_cache::{GpuCacheUpdate, GpuCacheUpdateList}; use crate::gpu_cache::{GpuCacheDebugChunk, GpuCacheDebugCmd}; use crate::gpu_types::{ScalingInstance, SvgFilterInstance, SVGFEFilterInstance, CopyInstance, PrimitiveInstanceData}; use crate::gpu_types::{BlurInstance, ClearInstance, CompositeInstance, ZBufferId}; -use crate::internal_types::{TextureSource, TextureSourceExternal, TextureCacheCategory, FrameId, FrameVec}; +use crate::internal_types::{TextureSource, TextureSourceExternal, FrameId, FrameVec}; #[cfg(any(feature = "capture", feature = "replay"))] use crate::internal_types::DebugOutput; use crate::internal_types::{CacheTextureId, FastHashMap, FastHashSet, RenderedDocument, ResultMsg}; @@ -1316,6 +1316,47 @@ impl Renderer { }; query.result.send(result).ok(); } + DebugQueryKind::Textures { category } => { + let mut texture_list = Vec::new(); + + self.device.begin_frame(); + self.device.bind_read_target_impl(self.read_fbo, DeviceIntPoint::zero()); + + for (id, item) in &self.texture_resolver.texture_cache_map { + if category.is_some() && category != Some(item.category) { + continue; + } + + let size = item.texture.get_dimensions(); + let format = item.texture.get_format(); + let buffer_size = (size.area() * format.bytes_per_pixel()) as usize; + let mut data = vec![0u8; buffer_size]; + let rect = size.cast_unit().into(); + self.device.attach_read_texture(&item.texture); + self.device.read_pixels_into(rect, format, &mut data); + + let category_str = match item.category { + TextureCacheCategory::Atlas => "atlas", + TextureCacheCategory::Standalone => "standalone", + TextureCacheCategory::PictureTile => "tile", + TextureCacheCategory::RenderTarget => "target", + }; + + let texture_msg = DebuggerTextureContent { + name: format!("{category_str}-{:02}", id.0), + category: item.category, + width: size.width as u32, + height: size.height as u32, + format, + data, + }; + texture_list.push(texture_msg); + } + self.device.reset_read_target(); + self.device.end_frame(); + + query.result.send(serde_json::to_string(&texture_list).unwrap()).ok(); + } } } DebugCommand::SaveCapture(..) | diff --git a/gfx/wr/webrender/src/texture_cache.rs b/gfx/wr/webrender/src/texture_cache.rs @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use api::{DirtyRect, ExternalImageType, ImageFormat, ImageBufferKind}; -use api::{DebugFlags, ImageDescriptor}; +use api::{DebugFlags, ImageDescriptor, TextureCacheCategory}; use api::units::*; #[cfg(test)] use api::{DocumentId, IdNamespace}; @@ -14,7 +14,7 @@ use crate::gpu_types::{ImageSource, UvRectKind}; use crate::internal_types::{ CacheTextureId, Swizzle, SwizzleSettings, FrameStamp, FrameId, TextureUpdateList, TextureUpdateSource, TextureSource, - TextureCacheAllocInfo, TextureCacheUpdate, TextureCacheCategory, + TextureCacheAllocInfo, TextureCacheUpdate, }; use crate::lru_cache::LRUCache; use crate::profiler::{self, TransactionProfile}; diff --git a/gfx/wr/webrender_api/Cargo.toml b/gfx/wr/webrender_api/Cargo.toml @@ -12,7 +12,7 @@ nightly = ["euclid/unstable", "serde/unstable"] serialize = [] deserialize = [] display_list_stats = [] -debugger = [] +debugger = ["serialize", "deserialize"] [dependencies] app_units = "0.7.3" diff --git a/gfx/wr/webrender_api/src/debugger.rs b/gfx/wr/webrender_api/src/debugger.rs @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use crate::{DebugFlags, PictureRect, DeviceRect}; +use crate::image::ImageFormat; // Shared type definitions between the WR crate and the debugger @@ -56,3 +57,13 @@ pub struct CompositorDebugInfo { pub enabled_z_layers: u64, pub tiles: Vec<CompositorDebugTile>, } + +#[derive(Debug, Serialize, Deserialize)] +pub struct DebuggerTextureContent { + pub name: String, + pub category: crate::TextureCacheCategory, + pub width: u32, + pub height: u32, + pub format: ImageFormat, + pub data: Vec<u8>, +} diff --git a/gfx/wr/webrender_api/src/lib.rs b/gfx/wr/webrender_api/src/lib.rs @@ -849,3 +849,14 @@ impl<'a> Drop for CrashAnnotatorGuard<'a> { } } } + +/// A little bit of extra information to make memory reports more useful +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serialize", derive(Serialize))] +#[cfg_attr(feature = "deserialize", derive(Deserialize))] +pub enum TextureCacheCategory { + Atlas, + Standalone, + PictureTile, + RenderTarget, +} diff --git a/gfx/wr/wrshell/Cargo.lock b/gfx/wr/wrshell/Cargo.lock @@ -119,6 +119,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] name = "ahash" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -969,6 +975,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] name = "derive_more" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1465,7 +1481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -2120,7 +2136,7 @@ dependencies = [ "byteorder-lite", "moxcms", "num-traits", - "png", + "png 0.18.0", "tiff", ] @@ -2378,6 +2394,15 @@ dependencies = [ [[package]] name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" @@ -3002,6 +3027,18 @@ dependencies = [ [[package]] name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags 1.2.1", + "crc32fast", + "deflate", + "miniz_oxide 0.3.7", +] + +[[package]] +name = "png" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" @@ -3010,7 +3047,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -5219,6 +5256,7 @@ dependencies = [ "eframe", "egui", "egui_tiles", + "png 0.16.8", "repl-ng", "reqwest", "ron", diff --git a/gfx/wr/wrshell/Cargo.toml b/gfx/wr/wrshell/Cargo.toml @@ -17,6 +17,7 @@ serde_json = "1" strprox = "0.3.3" repl-ng = "0.3" ron = "0.11.0" +png = "0.16" egui = "0.33" egui_tiles = { version = "0.14", features = ["serde"] } diff --git a/gfx/wr/wrshell/src/cli.rs b/gfx/wr/wrshell/src/cli.rs @@ -4,6 +4,9 @@ use repl_ng::Parameter; +use std::fs::File; +use std::io::BufWriter; + use crate::net; use crate::command; @@ -60,6 +63,23 @@ impl Cli { command::CommandOutput::SerdeDocument { content, .. } => { Ok(Some(content)) } + command::CommandOutput::Textures(textures) => { + for texture in textures { + let name = format!("{}.png", texture.name); + println!("saving {name:?}"); + + let file = File::create(name).unwrap(); + let ref mut w = BufWriter::new(file); + + let mut encoder = png::Encoder::new(w, texture.width, texture.height); + encoder.set_color(png::ColorType::RGBA); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header().unwrap(); + + writer.write_image_data(&texture.data).unwrap(); // Save + } + Ok(None) + } } }, ).with_help(desc.help); diff --git a/gfx/wr/wrshell/src/command.rs b/gfx/wr/wrshell/src/command.rs @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use std::collections::BTreeMap; +use webrender_api::debugger::DebuggerTextureContent; use crate::net; // Types for defining debug commands (and queries) that can be run in CLI or GUI mode @@ -44,6 +45,7 @@ pub enum CommandOutput { kind: String, content: String, }, + Textures(Vec<DebuggerTextureContent>), } pub struct ParamDescriptor { diff --git a/gfx/wr/wrshell/src/debug_commands.rs b/gfx/wr/wrshell/src/debug_commands.rs @@ -5,6 +5,7 @@ use crate::command::{Command, CommandList, CommandDescriptor}; use crate::command::{CommandContext, CommandOutput}; use webrender_api::DebugFlags; +use webrender_api::debugger::DebuggerTextureContent; // Implementation of a basic set of debug commands to demonstrate functionality @@ -16,6 +17,11 @@ pub fn register(cmd_list: &mut CommandList) { cmd_list.register_command(Box::new(GetSpatialTreeCommand)); cmd_list.register_command(Box::new(GetCompositeConfigCommand)); cmd_list.register_command(Box::new(GetCompositeViewCommand)); + cmd_list.register_command(Box::new(GetTexturesCommand { kind: None })); + cmd_list.register_command(Box::new(GetTexturesCommand { kind: Some("atlas") })); + cmd_list.register_command(Box::new(GetTexturesCommand { kind: Some("standalone") })); + cmd_list.register_command(Box::new(GetTexturesCommand { kind: Some("render-target") })); + cmd_list.register_command(Box::new(GetTexturesCommand { kind: Some("tile") })); } struct PingCommand; @@ -24,6 +30,7 @@ struct ToggleProfilerCommand; struct GetSpatialTreeCommand; struct GetCompositeConfigCommand; struct GetCompositeViewCommand; +struct GetTexturesCommand { kind: Option<&'static str> } impl Command for PingCommand { fn descriptor(&self) -> CommandDescriptor { @@ -198,3 +205,47 @@ impl Command for GetCompositeViewCommand { } } } + +impl Command for GetTexturesCommand { + fn descriptor(&self) -> CommandDescriptor { + CommandDescriptor { + name: match self.kind { + Some("atlas") => "get-atlas-textures", + Some("standalone") => "get-standalone-textures", + Some("render-target") => "get-target-textures", + Some("tile") => "get-tile-textures", + _ => "get-textures", + }, + help: "Fetch all gpu textures", + ..Default::default() + } + } + + fn run( + &mut self, + ctx: &mut CommandContext, + ) -> CommandOutput { + let kind = match self.kind { + Some("atlas") => "atlas-textures", + Some("standalone") => "standalone-textures", + Some("render-target") => "target-textures", + Some("tile") => "tile-textures", + _ => "textures", + }; + match ctx.net.get_with_query( + "query", + &[("type", kind)], + ) { + Ok(output) => { + let mut textures: Vec<DebuggerTextureContent> = serde_json::from_str( + output.unwrap().as_str() + ).unwrap(); + textures.sort_by(|a, b| a.name.cmp(&b.name)); + CommandOutput::Textures(textures) + } + Err(err) => { + CommandOutput::Err(err) + } + } + } +} diff --git a/gfx/wr/wrshell/src/gui/mod.rs b/gfx/wr/wrshell/src/gui/mod.rs @@ -5,11 +5,12 @@ mod debug_flags; mod profiler; mod shell; +mod textures; mod composite_view; use eframe::egui; use webrender_api::DebugFlags; -use webrender_api::debugger::{DebuggerMessage, ProfileCounterId, CompositorDebugInfo}; +use webrender_api::debugger::{DebuggerMessage, DebuggerTextureContent, ProfileCounterId, CompositorDebugInfo}; use crate::{command, net}; use std::collections::{HashMap, BTreeMap}; use std::fs; @@ -30,7 +31,7 @@ struct DataModel { cmd: String, log: Vec<String>, documents: Vec<Document>, - preview_doc_index: usize, + preview_doc_index: Option<usize>, profile_graphs: HashMap<ProfileCounterId, Graph>, } @@ -42,7 +43,7 @@ impl DataModel { cmd: String::new(), log: Vec::new(), documents: Vec::new(), - preview_doc_index: 0, + preview_doc_index: None, profile_graphs: HashMap::new(), } } @@ -154,11 +155,15 @@ impl Gui { let tabs = vec![ tiles.insert_pane(Tool::Profiler), tiles.insert_pane(Tool::Preview), + ]; + let side = vec![ + tiles.insert_pane(Tool::DebugFlags), tiles.insert_pane(Tool::Documents), ]; + let side = tiles.insert_vertical_tile(side); let main_tile = tiles.insert_tab_tile(tabs); let main_and_side = vec![ - tiles.insert_pane(Tool::DebugFlags), + side, main_tile, ]; let main_and_side = tiles.insert_horizontal_tile(main_and_side); @@ -243,6 +248,8 @@ impl eframe::App for Gui { } } + textures::prepare(self, ctx); + // Main menu bar egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { egui::MenuBar::new().ui(ui, |ui| { @@ -307,7 +314,7 @@ impl Gui { command::CommandOutput::TextDocument { title, content } => { let title = format!("{} [id {}]", title, self.doc_id); self.doc_id += 1; - self.data_model.preview_doc_index = self.data_model.documents.len(); + self.data_model.preview_doc_index = Some(self.data_model.documents.len()); self.data_model.documents.push( Document { title, @@ -320,7 +327,7 @@ impl Gui { command::CommandOutput::SerdeDocument { kind, ref content } => { let title = format!("Compositor [id {}]", self.doc_id); self.doc_id += 1; - self.data_model.preview_doc_index = self.data_model.documents.len(); + self.data_model.preview_doc_index = Some(self.data_model.documents.len()); let kind = match kind.as_str() { "composite-view" => { @@ -341,6 +348,9 @@ impl Gui { } ); } + command::CommandOutput::Textures(textures) => { + textures::add_textures(self, textures); + } } } None => { @@ -433,6 +443,10 @@ pub enum DocumentKind { Compositor { info: CompositorDebugInfo, }, + Texture { + content: DebuggerTextureContent, + handle: Option<egui::TextureHandle>, + } } pub struct Document { @@ -443,20 +457,32 @@ pub struct Document { fn do_documents_ui(app: &mut Gui, ui: &mut egui::Ui) { let width = ui.available_width(); for (i, doc) in app.data_model.documents.iter().enumerate() { + if let DocumentKind::Texture { .. } = doc.kind { + // Handle textures separately below. + continue; + } + let item = egui::Button::selectable( - app.data_model.preview_doc_index == i, + app.data_model.preview_doc_index == Some(i), &doc.title, ).min_size(egui::vec2(width, 20.0)); if ui.add(item).clicked() { - app.data_model.preview_doc_index = i; + app.data_model.preview_doc_index = Some(i); } } + + textures::texture_list_ui(app, ui); } fn do_preview_ui(app: &mut Gui, ui: &mut egui::Ui) { - if app.data_model.preview_doc_index < app.data_model.documents.len() { - let doc = &app.data_model.documents[app.data_model.preview_doc_index]; + if let Some(idx) = app.data_model.preview_doc_index { + if idx >= app.data_model.documents.len() { + app.data_model.preview_doc_index = None; + return; + } + + let doc = &app.data_model.documents[idx]; match &doc.kind { DocumentKind::Text { content } => { @@ -466,12 +492,16 @@ fn do_preview_ui(app: &mut Gui, ui: &mut egui::Ui) { } DocumentKind::Compositor { .. } => { // We need to handle compositor separately due to borrow checker - let preview_doc_index = app.data_model.preview_doc_index; if let Some(Document { kind: DocumentKind::Compositor { info }, .. }) = - app.data_model.documents.get_mut(preview_doc_index) { + app.data_model.documents.get_mut(idx) { composite_view::ui(ui, info); } } + DocumentKind::Texture { content, handle } => { + if let Some(handle) = handle { + textures::texture_viewer_ui(ui, &content, &handle); + } + } } } } diff --git a/gfx/wr/wrshell/src/gui/textures.rs b/gfx/wr/wrshell/src/gui/textures.rs @@ -0,0 +1,184 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use super::{Gui, Document, DocumentKind}; +use webrender_api::debugger::DebuggerTextureContent; +use webrender_api::TextureCacheCategory; + +pub fn texture_viewer_ui(ui: &mut egui::Ui, image: &DebuggerTextureContent, handle: &egui::TextureHandle) { + ui.label(format!("Size: {}x{}, Format {:?}", image.width, image.height, image.format)); + + egui::ScrollArea::both().show(ui, |ui| { + ui.image(egui::ImageSource::Texture( + egui::load::SizedTexture::new( + handle.id(), + egui::vec2(image.width as f32, image.height as f32) + ) + )); + }); +} + +pub fn texture_list_ui(app: &mut Gui, ui: &mut egui::Ui) { + texture_list_inner(app, ui, TextureCacheCategory::Atlas); + texture_list_inner(app, ui, TextureCacheCategory::Standalone); + texture_list_inner(app, ui, TextureCacheCategory::RenderTarget); + texture_list_inner(app, ui, TextureCacheCategory::PictureTile); +} + +fn texture_category_query(category: TextureCacheCategory) -> &'static str { + match category { + TextureCacheCategory::Atlas => "atlas-textures", + TextureCacheCategory::Standalone => "standalone-textures", + TextureCacheCategory::PictureTile => "tile-textures", + TextureCacheCategory::RenderTarget => "target-textures", + } +} + +fn texture_category_label(category: TextureCacheCategory) -> &'static str { + match category { + TextureCacheCategory::Atlas => "Atlases", + TextureCacheCategory::Standalone => "Standalone", + TextureCacheCategory::PictureTile => "Tiles", + TextureCacheCategory::RenderTarget => "Render targets", + } +} + +fn texture_list_inner(app: &mut Gui, ui: &mut egui::Ui, category: TextureCacheCategory) { + let width = ui.available_width(); + + let cursor = ui.cursor().min; + let refresh_rect = egui::Rect { + min: egui::Pos2::new(cursor.x + width - 20.0, cursor.y), + max: egui::Pos2::new(cursor.x + width, cursor.y + 20.0), + }; + let refresh_button = egui::widgets::Button::new("↓"); + if ui.place(refresh_rect, refresh_button).clicked() { + let query_result = app.net.get_with_query( + "query", &[("type", texture_category_query(category))] + ); + + if let Ok(Some(msg)) = query_result { + app.data_model.preview_doc_index = None; + app.data_model.documents.retain(|doc| !doc_is_texture(doc, category)); + + // Note: deserializing the textures takes a long time in + // debug builds. + let new_textures = serde_json::from_str(msg.as_str()).unwrap(); + add_textures(app, new_textures); + } + } + + egui::CollapsingHeader::new(texture_category_label(category)).default_open(true).show(ui, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + for (i, doc) in app.data_model.documents.iter().enumerate() { + if !doc_is_texture(doc, category) { + continue; + } + + let item = egui::Button::selectable( + app.data_model.preview_doc_index == Some(i), + &doc.title, + ).min_size(egui::vec2(width - 20.0, 20.0)); + + if ui.add(item).clicked() { + app.data_model.preview_doc_index = Some(i); + } + } + }); + }); +} + +pub fn add_textures( + app: &mut Gui, + mut textures: Vec<DebuggerTextureContent>, +) { + textures.sort_by(|a, b| a.name.cmp(&b.name)); + for texture in textures { + app.data_model.documents.push(Document { + title: texture.name.clone(), + kind: DocumentKind::Texture { + content: texture, + handle: None, + } + }); + } +} + +/// Perform uploads if need be. Happens earlier in the update because it needs +/// access to the egui context. +pub fn prepare(app: &mut super::Gui, ctx: &egui::Context) { + if let Some(idx) = app.data_model.preview_doc_index { + if idx >= app.data_model.documents.len() { + return; + } + + let DocumentKind::Texture { content, handle } = &mut app.data_model.documents[idx].kind else { + return; + }; + + if handle.is_some() { + return; + } + + if let Some(gpu_texture) = upload_texture(ctx, content) { + *handle = Some(gpu_texture) + } + } +} + +fn upload_texture( + ctx: &egui::Context, + texture: &DebuggerTextureContent, +) -> Option<egui::TextureHandle> { + use webrender_api::ImageFormat; + + let color_image = match texture.format { + ImageFormat::RGBA8 => { + egui::ColorImage::from_rgba_unmultiplied( + [texture.width as usize, texture.height as usize], + &texture.data, + ) + } + ImageFormat::BGRA8 => { + // Convert BGRA to RGBA + let mut rgba_data = texture.data.clone(); + for pixel in rgba_data.chunks_exact_mut(4) { + pixel.swap(0, 2); // Swap B and R + } + egui::ColorImage::from_rgba_unmultiplied( + [texture.width as usize, texture.height as usize], + &rgba_data, + ) + } + ImageFormat::R8 => { + // Convert grayscale to RGBA + let rgba_data: Vec<u8> = texture.data.iter() + .flat_map(|&gray| [gray, gray, gray, 255]) + .collect(); + egui::ColorImage::from_rgba_unmultiplied( + [texture.width as usize, texture.height as usize], + &rgba_data, + ) + } + _ => { + println!("Unsupported texture format: {:?}", texture.format); + return None; + } + }; + + Some(ctx.load_texture( + &texture.name, + color_image, + egui::TextureOptions::default(), + )) +} + +fn doc_is_texture(doc: &Document, kind: TextureCacheCategory) -> bool { + match doc.kind { + DocumentKind::Texture { content: DebuggerTextureContent { category, .. }, .. } => { + category == kind + }, + _ => false, + } +}