mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-12 13:03:44 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
300d44d52b |
No files matched your search
@@ -8,6 +8,22 @@ use super::{
|
||||
Renderer, SectionGpu, SectionKey, TerrainVertex, arena, flat, lod, texture_bytes, visbuffer,
|
||||
};
|
||||
|
||||
/// What the backend's allocator holds (`Renderer::gpu_memory_by_label`).
|
||||
pub struct GpuMemory {
|
||||
pub allocated: u64,
|
||||
pub reserved: u64,
|
||||
/// Bytes by label of what holds them, largest first.
|
||||
pub labels: Vec<(String, u64)>,
|
||||
pub blocks: Vec<GpuBlock>,
|
||||
}
|
||||
|
||||
/// One block of device memory: its size, the bytes allocated in it and its largest allocations.
|
||||
pub struct GpuBlock {
|
||||
pub size: u64,
|
||||
pub used: u64,
|
||||
pub names: Vec<String>,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
/// Sections with GPU buffers, for soak tests watching residency.
|
||||
pub fn resident_sections(&self) -> usize {
|
||||
@@ -117,19 +133,45 @@ impl Renderer {
|
||||
/// Device memory the backend's allocator holds, by the label of what holds
|
||||
/// it, largest first, with its allocated and reserved totals (Vulkan and
|
||||
/// DirectX 12; none on Metal and OpenGL) (`horizon-ctl memory`).
|
||||
pub fn gpu_memory_by_label(&self) -> Option<(u64, u64, Vec<(String, u64)>)> {
|
||||
pub fn gpu_memory_by_label(&self) -> Option<GpuMemory> {
|
||||
let report = self.device.generate_allocator_report()?;
|
||||
let mut by_label: HashMap<String, u64> = HashMap::new();
|
||||
for a in report.allocations {
|
||||
*by_label.entry(a.name).or_default() += a.size;
|
||||
for a in &report.allocations {
|
||||
*by_label.entry(a.name.clone()).or_default() += a.size;
|
||||
}
|
||||
let mut labels: Vec<_> = by_label.into_iter().collect();
|
||||
labels.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
|
||||
Some((
|
||||
report.total_allocated_bytes,
|
||||
report.total_reserved_bytes,
|
||||
// Each block with what is used in it, to see where reserved memory sits empty.
|
||||
let blocks = report
|
||||
.blocks
|
||||
.iter()
|
||||
.map(|block| {
|
||||
let inside = report.allocations.get(block.allocations.clone());
|
||||
let used: u64 = inside.iter().flat_map(|a| a.iter()).map(|a| a.size).sum();
|
||||
let mut names: Vec<(&str, u64)> = inside
|
||||
.iter()
|
||||
.flat_map(|a| a.iter())
|
||||
.map(|a| (a.name.as_str(), a.size))
|
||||
.collect();
|
||||
names.sort_by_key(|a| std::cmp::Reverse(a.1));
|
||||
let names = names
|
||||
.iter()
|
||||
.take(3)
|
||||
.map(|(n, s)| format!("{n} {s}"))
|
||||
.collect();
|
||||
GpuBlock {
|
||||
size: block.size,
|
||||
used,
|
||||
names,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Some(GpuMemory {
|
||||
allocated: report.total_allocated_bytes,
|
||||
reserved: report.total_reserved_bytes,
|
||||
labels,
|
||||
))
|
||||
blocks,
|
||||
})
|
||||
}
|
||||
/// Heap bytes of the renderer's per-section bookkeeping (`horizon-ctl memory`):
|
||||
/// resident sections, far tiles, block entities and light sources by section.
|
||||
@@ -162,3 +204,47 @@ impl Renderer {
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// Shared blocks stay at or under 16 MiB, so a block a few long-lived buffers hold is
|
||||
/// small, and a larger buffer gets a block of its own size (#15723).
|
||||
#[test]
|
||||
fn larger_buffers_get_their_own_block_and_shared_blocks_stay_small() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
|
||||
return;
|
||||
};
|
||||
let Ok((device, _queue)) =
|
||||
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
memory_hints: super::super::surface_setup::MEMORY_HINTS,
|
||||
..Default::default()
|
||||
}))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let buffer = |mb: u64| {
|
||||
device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("census test"),
|
||||
size: mb << 20,
|
||||
usage: wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
})
|
||||
};
|
||||
// Six-MiB buffers: enough of them to open the largest shared block size.
|
||||
let buffers: Vec<_> = (0..12).map(|_| buffer(6)).chain([buffer(40)]).collect();
|
||||
let Some(report) = device.generate_allocator_report() else {
|
||||
return; // Metal and OpenGL have none.
|
||||
};
|
||||
for block in &report.blocks {
|
||||
let inside = &report.allocations[block.allocations.clone()];
|
||||
assert!(
|
||||
block.size <= 16 << 20 || inside.len() == 1,
|
||||
"a {} MiB block shared by {} allocations",
|
||||
block.size >> 20,
|
||||
inside.len()
|
||||
);
|
||||
}
|
||||
drop(buffers);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,16 @@ pub(super) mod ray_query;
|
||||
use std::sync::Arc;
|
||||
use winit::window::Window;
|
||||
|
||||
/// Video memory the allocator shares out in blocks of 4-16 MiB (Vulkan and DirectX 12); a
|
||||
/// buffer or texture larger than a block gets a block of exactly its size, which is freed
|
||||
/// with it. With blocks of 8-64 MiB (`MemoryHints::MemoryUsage`) the size grew with each
|
||||
/// block opened, and the few small buffers that live all session (section tests, clouds,
|
||||
/// the staging belt) each kept a 32 or 64 MiB block from being freed: some 140 of 540 MB
|
||||
/// reserved were empty in a loaded world at render distance 12 (#15723).
|
||||
pub(super) const MEMORY_HINTS: wgpu::MemoryHints = wgpu::MemoryHints::Manual {
|
||||
suballocated_device_memory_block_size: 4 << 20..16 << 20,
|
||||
};
|
||||
|
||||
/// Sets the window surface up on `device`: the swapchain format (`surface_format`), the
|
||||
/// copies screenshots and frame generation make, and Mailbox when the surface offers it
|
||||
/// (`keep_mode`, a replaced renderer's mode, first). `caps` are the surface's capabilities
|
||||
@@ -167,10 +177,7 @@ pub(super) async fn open_adapter(
|
||||
label: Some("Horizon device"),
|
||||
required_features: features,
|
||||
required_limits: limits,
|
||||
// Video memory in blocks of 8-64 MiB instead of 128-256 MiB (Vulkan and DirectX 12):
|
||||
// with the larger blocks a few small buffers kept whole blocks the arena's pages had
|
||||
// left, and over half the video memory reserved was unused (1,344 MiB for 500).
|
||||
memory_hints: wgpu::MemoryHints::MemoryUsage,
|
||||
memory_hints: MEMORY_HINTS,
|
||||
experimental_features: ray_query::experimental(features),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -245,6 +245,8 @@ struct Gpu {
|
||||
allocated_mb: f64,
|
||||
reserved_mb: f64,
|
||||
by_label_mb: Vec<(String, f64)>,
|
||||
/// Blocks of device memory, largest first: size, bytes used, largest allocations.
|
||||
blocks: Vec<(f64, f64, Vec<String>)>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -571,17 +573,24 @@ impl Game<'_> {
|
||||
let [arenas, vertices] = r.section_vertex_bytes();
|
||||
let cpu = r.cpu_census();
|
||||
let cpu_bytes: usize = cpu.iter().map(|(_, v)| *v).sum();
|
||||
let gpu = r
|
||||
.gpu_memory_by_label()
|
||||
.map(|(allocated, reserved, labels)| Gpu {
|
||||
allocated_mb: mb(allocated),
|
||||
reserved_mb: mb(reserved),
|
||||
by_label_mb: labels
|
||||
let gpu = r.gpu_memory_by_label().map(|m| {
|
||||
let mut blocks: Vec<_> = m.blocks.iter().collect();
|
||||
blocks.sort_by_key(|b| std::cmp::Reverse(b.size));
|
||||
Gpu {
|
||||
allocated_mb: mb(m.allocated),
|
||||
reserved_mb: mb(m.reserved),
|
||||
by_label_mb: m
|
||||
.labels
|
||||
.iter()
|
||||
.take(40)
|
||||
.map(|(label, bytes)| (label.to_string(), mb(*bytes)))
|
||||
.collect(),
|
||||
});
|
||||
blocks: blocks
|
||||
.iter()
|
||||
.map(|b| (mb(b.size), mb(b.used), b.names.clone()))
|
||||
.collect(),
|
||||
}
|
||||
});
|
||||
let renderer = Renderer {
|
||||
section_arenas_mb: mb(arenas),
|
||||
section_vertices_mb: mb(vertices),
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ The reply is built in `crates/horizon-client/src/game/memory.rs`. Sizes are MiB
|
||||
| `far_tiles` | Far terrain tiles: `active` and `cached`, and the tiles `kept` for a return with their `kept_mb` |
|
||||
| `renderer.section_arenas_mb`, `section_vertices_mb` | The GPU buffers of the section vertex arenas, and the vertices that resident sections and far tiles hold in them |
|
||||
| `renderer.cpu` | The renderer's CPU-side bookkeeping: `resident_sections_mb`, `far_tiles_mb`, `block_entities_mb`, `light_sources_mb` |
|
||||
| `renderer.gpu` | The GPU allocator's `allocated_mb` and `reserved_mb`, and the 40 largest resources as `["label", mb]` rows in `by_label_mb`; `null` without Vulkan or DirectX 12 |
|
||||
| `renderer.gpu` | The GPU allocator's `allocated_mb` and `reserved_mb`, and the 40 largest resources as `["label", mb]` rows in `by_label_mb`, and `blocks`, each block of device memory as `[size_mb, used_mb, its largest allocations]` (reserved memory sitting empty shows as `used_mb` below `size_mb`); `null` without Vulkan or DirectX 12 |
|
||||
| `named_mb`, `unattributed_mb` | `named_mb` adds up the columns (a section both the window and its server thread hold once), `server_generation`, `entities`, `players`, `chunk_send`, the two queues, `renderer.cpu` and the far tiles' `kept_mb`. `unattributed_mb` is `process.resident_mb` less that; it can be negative where a census overlaps another, and `null` where the resident set is not read |
|
||||
| `heap_by_tag_mb` | Only in an `alloc-census` build, otherwise `null`: see [the heap census](#who-allocated-it-the-heap-census) |
|
||||
|
||||
|
||||
Reference in new issue
Block a user