/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
//! This module contains the convoluted logic that goes into uploading content into //! the texture cache's textures. //! //! We need to support various combinations of code paths depending on the quirks of //! each hardware/driver configuration: //! - direct upload, //! - staged upload via a pixel buffer object, //! - staged upload via a direct upload to a staging texture where PBO's aren't supported, //! - copy from the staging to destination textures, either via blits or batched draw calls. //! //! Conceptually a lot of this logic should probably be in the device module, but some code //! here relies on submitting draw calls via the renderer.
use std::mem; use std::collections::VecDeque; use std::sync::Arc; use std::time::Duration; use euclid::{Transform3D, point2}; use malloc_size_of::MallocSizeOfOps; use api::units::*; use api::{ExternalImageSource, ImageBufferKind, ImageFormat}; usecrate::renderer::{
Renderer, VertexArrayKind, RendererStats, TextureSampler, TEXTURE_CACHE_DBG_CLEAR_COLOR
}; usecrate::internal_types::{
FastHashMap, TextureUpdateSource, Swizzle, TextureCacheUpdate,
CacheTextureId, RenderTargetInfo,
}; usecrate::device::{
Device, UploadMethod, Texture, DrawTarget, UploadStagingBuffer, TextureFlags, TextureUploader,
TextureFilter,
}; usecrate::gpu_types::CopyInstance; usecrate::batch::BatchTextures; usecrate::texture_pack::{GuillotineAllocator, FreeRectSlice}; usecrate::profiler; usecrate::render_api::MemoryReport;
/// Upload a number of items to texture cache textures. /// /// This is the main entry point of the texture cache upload code. /// See also the module documentation for more information. pubfn upload_to_texture_cache(
renderer: &mut Renderer,
update_list: FastHashMap<CacheTextureId, Vec<TextureCacheUpdate>>,
) { if update_list.is_empty() { return;
}
// A list of copies that must be performed from the temporary textures to the texture cache. letmut batch_upload_copies = Vec::new();
// For each texture format, this stores a list of staging buffers // and a texture allocator for packing the buffers. letmut batch_upload_buffers = FastHashMap::default();
// For best performance we use a single TextureUploader for all uploads. // This allows us to fill PBOs more efficiently and therefore allocate fewer PBOs. letmut uploader = renderer.device.upload_texture(
&mut renderer.texture_upload_pbo_pool,
);
let num_updates = update_list.len();
for (texture_id, updates) in update_list { let texture = &renderer.texture_resolver.texture_cache_map[&texture_id].texture; for update in updates { let TextureCacheUpdate { rect, stride, offset, format_override, source } = update; letmut arc_data = None; let dummy_data; let data = match source {
TextureUpdateSource::Bytes { ref data } => {
arc_data = Some(data.clone());
&data[offset as usize ..]
}
TextureUpdateSource::External { id, channel_index } => { let handler = renderer.external_image_handler
.as_mut()
.expect("Found external image, but no handler set!"); // The filter is only relevant for NativeTexture external images. match handler.lock(id, channel_index, false).source {
ExternalImageSource::RawData(data) => {
&data[offset as usize ..]
}
ExternalImageSource::Invalid => { // Create a local buffer to fill the pbo. let bpp = texture.get_format().bytes_per_pixel(); let width = stride.unwrap_or(rect.width() * bpp); let total_size = width * rect.height(); // WR haven't support RGBAF32 format in texture_cache, so // we use u8 type here.
dummy_data = vec![0xFFu8; total_size as usize];
&dummy_data
}
ExternalImageSource::NativeTexture(eid) => {
panic!("Unexpected external texture {:?} for the texture cache update of {:?}", eid, id);
}
}
}
TextureUpdateSource::DebugClear => { let draw_target = DrawTarget::from_texture(
texture, false,
);
renderer.device.bind_draw_target(draw_target);
renderer.device.clear_target(
Some(TEXTURE_CACHE_DBG_CLEAR_COLOR),
None,
Some(draw_target.to_framebuffer_rect(update.rect.to_i32()))
);
// Flush all uploads, batched or otherwise. let flush_start_time = zeitstempel::now();
uploader.flush(&mut renderer.device);
stats.upload_time += zeitstempel::now() - flush_start_time;
if !batch_upload_copies.is_empty() { // Copy updates that were batch uploaded to their correct destination in the texture cache. // Sort them by destination and source to minimize framebuffer binding changes.
batch_upload_copies.sort_unstable_by_key(|b| (b.dest_texture_id.0, b.src_texture_index));
let gpu_copy_start = zeitstempel::now();
if renderer.device.use_draw_calls_for_texture_copy() { // Some drivers have a very high CPU overhead when submitting hundreds of small blit // commands (low end intel drivers on Windows for example can take take 100+ ms submitting a // few hundred blits). In this case we do the copy with batched draw calls.
copy_from_staging_to_cache_using_draw_calls(
renderer,
&mut stats,
&batch_upload_textures,
batch_upload_copies,
);
} else {
copy_from_staging_to_cache(
renderer,
&batch_upload_textures,
batch_upload_copies,
);
}
for texture in batch_upload_textures.drain(..) {
renderer.staging_texture_pool.return_texture(texture);
}
// Update the profile counters. We use add instead of set because // this function can be called several times per frame. // We don't update the counters when their value is zero, so that // the profiler can treat them as events and we can get notified // when they happen.
let upload_total = zeitstempel::now() - upload_total_start;
renderer.profile.add(
profiler::TOTAL_UPLOAD_TIME,
profiler::ns_to_ms(upload_total)
);
if num_updates > 0 {
renderer.profile.add(profiler::TEXTURE_UPLOADS, num_updates);
}
if stats.bytes_uploaded > 0 {
renderer.profile.add(
profiler::TEXTURE_UPLOADS_MEM,
profiler::bytes_to_mb(stats.bytes_uploaded)
);
}
if stats.cpu_copy_time > 0 {
renderer.profile.add(
profiler::UPLOAD_CPU_COPY_TIME,
profiler::ns_to_ms(stats.cpu_copy_time)
);
} if stats.upload_time > 0 {
renderer.profile.add(
profiler::UPLOAD_TIME,
profiler::ns_to_ms(stats.upload_time)
);
} if stats.texture_alloc_time > 0 {
renderer.profile.add(
profiler::STAGING_TEXTURE_ALLOCATION_TIME,
profiler::ns_to_ms(stats.texture_alloc_time)
);
} if stats.cpu_buffer_alloc_time > 0 {
renderer.profile.add(
profiler::CPU_TEXTURE_ALLOCATION_TIME,
profiler::ns_to_ms(stats.cpu_buffer_alloc_time)
);
} if stats.num_draw_calls > 0{
renderer.profile.add(
profiler::UPLOAD_NUM_COPY_BATCHES,
stats.num_draw_calls
);
}
if stats.gpu_copy_commands_time > 0 {
renderer.profile.add(
profiler::UPLOAD_GPU_COPY_TIME,
profiler::ns_to_ms(stats.gpu_copy_commands_time)
);
}
let add_markers = profiler::thread_is_being_profiled(); if add_markers && stats.bytes_uploaded > 0 { let details = format!("{} bytes uploaded, {} items", stats.bytes_uploaded, stats.items_uploaded);
profiler::add_text_marker(&"Texture uploads", &details, Duration::from_nanos(upload_total));
}
}
// Allocate a region within the staging buffer for this update. If there is // no room in an existing buffer then allocate another texture and buffer. let (slice, origin) = match allocator.allocate(&update_rect.size()) {
Some((slice, origin)) => (slice, origin),
None => { let new_slice = FreeRectSlice(buffers.len() as u32);
allocator.extend(new_slice, BATCH_UPLOAD_TEXTURE_SIZE, update_rect.size());
let texture_alloc_time_start = zeitstempel::now(); let staging_texture = staging_texture_pool.get_texture(device, texture.get_format());
stats.texture_alloc_time = zeitstempel::now() - texture_alloc_time_start;
let texture_index = batch_upload_textures.len();
batch_upload_textures.push(staging_texture);
unsafe { let memcpy_start_time = zeitstempel::now(); let bpp = texture.get_format().bytes_per_pixel() as usize; let width_bytes = update_rect.width() as usize * bpp; let src_stride = update_stride.map_or(width_bytes, |stride| {
assert!(stride >= 0);
stride as usize
}); let src_size = (update_rect.height() as usize - 1) * src_stride + width_bytes;
assert!(src_size <= data.len());
let src: &[mem::MaybeUninit<u8>] = std::slice::from_raw_parts(data.as_ptr() as *const _, src_size); let (dst_stride, dst) = match &mut buffer.staging_buffer {
StagingBufferKind::Pbo(buffer) => (
buffer.get_stride(),
buffer.get_mapping(),
),
StagingBufferKind::CpuBuffer { bytes } => (
BATCH_UPLOAD_TEXTURE_SIZE.width as usize * bpp,
&mut bytes[..],
),
StagingBufferKind::Image { .. } => unreachable!(),
};
// copy the data line-by-line in to the buffer so that we do not overwrite // any other region of the buffer. for y in0..allocated_rect.height() as usize { let src_start = y * src_stride; let src_end = src_start + width_bytes; let dst_start = (allocated_rect.min.y as usize + y as usize) * dst_stride +
allocated_rect.min.x as usize * bpp; let dst_end = dst_start + width_bytes;
/// Take this code path instead of copying into a staging CPU buffer when the image /// we would copy is large enough that it's unlikely anything else would fit in the /// buffer, therefore we might as well copy directly from the source image's pixels. fn skip_staging_buffer<'a>(
device: &mut Device,
staging_texture_pool: &mut UploadTexturePool,
update_rect: DeviceIntRect,
stride: Option<i32>,
data: Arc<Vec<u8>>,
dest_texture_id: CacheTextureId,
texture: &Texture,
batch_upload_buffers: &mut FastHashMap<ImageFormat, (GuillotineAllocator, Vec<BatchUploadBuffer<'a>>)>,
batch_upload_textures: &mut Vec<Texture>,
batch_upload_copies: &mut Vec<BatchUploadCopy>,
stats: &mut UploadStats
) { let (_, buffers) = batch_upload_buffers.entry(texture.get_format())
.or_insert_with(|| (GuillotineAllocator::new(None), Vec::new()));
let texture_alloc_time_start = zeitstempel::now(); let staging_texture = staging_texture_pool.get_texture(device, texture.get_format());
stats.texture_alloc_time = zeitstempel::now() - texture_alloc_time_start;
let texture_index = batch_upload_textures.len();
batch_upload_textures.push(staging_texture);
renderer.device.copy_texture_sub_region(
&batch_upload_textures[copy.src_texture_index],
copy.src_offset.x as _,
copy.src_offset.y as _,
dest_texture,
copy.dest_offset.x as _,
copy.dest_offset.y as _,
copy.size.width as _,
copy.size.height as _,
);
}
}
/// Generate and submit composite shader batches to copy from /// the staging textures to the destination cache textures. /// /// If this shows up in GPU time ptofiles we could replace it with /// a simpler shader (composite.glsl is already quite simple). fn copy_from_staging_to_cache_using_draw_calls(
renderer: &mut Renderer,
stats: &mut UploadStats,
batch_upload_textures: &[Texture],
batch_upload_copies: Vec<BatchUploadCopy>,
) { letmut copy_instances = Vec::new(); letmut prev_src = None; letmut prev_dst = None; letmut dst_texture_size = DeviceSize::new(0.0, 0.0);
for copy in batch_upload_copies {
let src_changed = prev_src != Some(copy.src_texture_index); let dst_changed = prev_dst != Some(copy.dest_texture_id);
/// A very basic pool to avoid reallocating staging textures as well as staging /// CPU side buffers. pubstruct UploadTexturePool { /// The textures in the pool associated with a last used frame index. /// /// The outer array corresponds to each of teh three supported texture formats.
textures: [VecDeque<(Texture, u64)>; BATCH_UPLOAD_FORMAT_COUNT], // Frame at which to deallocate some textures if there are too many in the pool, // for each format.
delay_texture_deallocation: [u64; BATCH_UPLOAD_FORMAT_COUNT],
current_frame: u64,
/// Temporary buffers that are used when using staging uploads + glTexImage2D. /// /// Temporary buffers aren't used asynchronously so they can be reused every frame. /// To keep things simple we always allocate enough memory for formats with four bytes /// per pixel (more than we need for alpha-only textures but it works just as well).
temporary_buffers: Vec<Vec<mem::MaybeUninit<u8>>>,
min_temporary_buffers: usize,
delay_buffer_deallocation: u64,
}
/// Create or reuse a staging texture. /// /// See also return_texture. pubfn get_texture(&mutself, device: &mut Device, format: ImageFormat) -> Texture {
// First try to reuse a texture from the pool. // "available" here means hasn't been used for 2 frames to avoid stalls. // No need to scan the vector. Newer textures are always pushed at the back // of the vector so we know the first element is the least recently used. let format_idx = self.format_index(format); let can_reuse = self.textures[format_idx].get(0)
.map(|tex| self.current_frame - tex.1 > 2)
.unwrap_or(false);
if can_reuse { returnself.textures[format_idx].pop_front().unwrap().0;
}
// If we couldn't find an available texture, create a new one.
device.create_texture(
ImageBufferKind::Texture2D,
format,
BATCH_UPLOAD_TEXTURE_SIZE.width,
BATCH_UPLOAD_TEXTURE_SIZE.height,
TextureFilter::Nearest, // Currently we need render target support as we always use glBlitFramebuffer // to copy the texture data. Instead, we should use glCopyImageSubData on some // platforms, and avoid creating the FBOs in that case.
Some(RenderTargetInfo { has_depth: false }),
)
}
/// Hand the staging texture back to the pool after being done with uploads. /// /// The texture must have been obtained from this pool via get_texture. pubfn return_texture(&mutself, texture: Texture) { let format_idx = self.format_index(texture.get_format()); self.textures[format_idx].push_back((texture, self.current_frame));
}
/// Create or reuse a temporary CPU buffer. /// /// These buffers are used in the batched upload path when PBOs are not supported. /// Content is first written to the temporary buffer and uploaded via a single /// glTexSubImage2D call. pubfn get_temporary_buffer(&mutself) -> Vec<mem::MaybeUninit<u8>> { let buffer = self.temporary_buffers.pop().unwrap_or_else(|| {
vec![mem::MaybeUninit::new(0); BATCH_UPLOAD_TEXTURE_SIZE.area() as usize * 4]
}); self.min_temporary_buffers = self.min_temporary_buffers.min(self.temporary_buffers.len());
buffer
}
/// Return memory that was obtained from this pool via get_temporary_buffer. pubfn return_temporary_buffer(&mutself, buffer: Vec<mem::MaybeUninit<u8>>) {
assert_eq!(buffer.len(), BATCH_UPLOAD_TEXTURE_SIZE.area() as usize * 4); self.temporary_buffers.push(buffer);
}
/// Deallocate this pool's CPU and GPU memory. pubfn delete_textures(&mutself, device: &mut Device) { for format in &mutself.textures { whilelet Some(texture) = format.pop_back() {
device.delete_texture(texture.0)
}
} self.temporary_buffers.clear();
}
/// Deallocate some textures if there are too many for a long time. pubfn end_frame(&mutself, device: &mut Device) { for format_idx in0..self.textures.len() { // Count the number of reusable staging textures. // if it stays high for a large number of frames, truncate it back to 8-ish // over multiple frames.
letmut num_reusable_textures = 0; for texture in &self.textures[format_idx] { ifself.current_frame - texture.1 > 2 {
num_reusable_textures += 1;
}
}
if num_reusable_textures < 8 { // Don't deallocate textures for another 120 frames. self.delay_texture_deallocation[format_idx] = self.current_frame + 120;
}
// Deallocate up to 4 staging textures every frame. let to_remove = ifself.current_frame > self.delay_texture_deallocation[format_idx] {
num_reusable_textures.min(4)
} else { 0
};
for _ in0..to_remove { let texture = self.textures[format_idx].pop_front().unwrap().0;
device.delete_texture(texture);
}
}
// Similar logic for temporary CPU buffers. Our calls to get and return // temporary buffers should have been balanced for this frame, but the call // get_temporary_buffer will allocate a buffer if the vec is empty. Since we // carry these buffers from frame to frame, we keep track of the smallest // length of the temporary_buffers vec that we encountered this frame. Those // buffers were not touched and we deallocate some if there are a lot of them. let unused_buffers = self.min_temporary_buffers; if unused_buffers < 8 { self.delay_buffer_deallocation = self.current_frame + 120;
} let to_remove = ifself.current_frame > self.delay_buffer_deallocation {
unused_buffers.min(4)
} else { 0
}; for _ in0..to_remove { // Unlike textures it doesn't matter whether we pop from the front or back // of the vector. self.temporary_buffers.pop();
}
}
pubfn report_memory_to(&self, report: &mut MemoryReport, size_op_funs: &MallocSizeOfOps) { for buf in &self.temporary_buffers {
report.upload_staging_memory += unsafe { (size_op_funs.size_of_op)(buf.as_ptr() as *const _) };
}
for format in &self.textures { for texture in format {
report.upload_staging_textures += texture.0.size_in_bytes();
}
}
}
}
#[derive(Debug)] enum StagingBufferKind<'a> {
Pbo(UploadStagingBuffer<'a>),
CpuBuffer { bytes: Vec<mem::MaybeUninit<u8>> },
Image { bytes: Arc<Vec<u8>>, stride: Option<i32> },
} #[derive(Debug)] struct BatchUploadBuffer<'a> {
staging_buffer: StagingBufferKind<'a>,
texture_index: usize, // A rectangle containing all items going into this staging texture, so // that we can avoid uploading the entire area if we are using glTexSubImage2d.
upload_rect: DeviceIntRect,
}
// On some devices performing many small texture uploads is slow, so instead we batch // updates in to a small number of uploads to temporary textures, then copy from those // textures to the correct place in the texture cache. // A list of temporary textures that batches of updates are uploaded to. #[derive(Debug)] struct BatchUploadCopy { // Index within batch_upload_textures
src_texture_index: usize,
src_offset: DeviceIntPoint,
dest_texture_id: CacheTextureId,
dest_offset: DeviceIntPoint,
size: DeviceIntSize,
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.20 Sekunden
(vorverarbeitet am 2026-08-25)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.