Add a unified GPU allocator

This commit is contained in:
Patrick Walton 2020-06-23 12:30:02 -07:00
parent 754a44ae22
commit 7771fd877d
3 changed files with 443 additions and 3 deletions

View File

@ -10,7 +10,9 @@ homepage = "https://github.com/servo/pathfinder"
[dependencies]
bitflags = "1.0"
fxhash = "0.2"
half = "1.5"
log = "0.4"
[dependencies.image]
version = "0.23"

400
gpu/src/allocator.rs Normal file
View File

@ -0,0 +1,400 @@
// pathfinder/gpu/src/gpu/allocator.rs
//
// Copyright © 2020 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! GPU memory management.
use crate::{BufferData, BufferTarget, BufferUploadMode, Device, TextureFormat};
use fxhash::FxHashMap;
use pathfinder_geometry::vector::Vector2I;
use std::collections::VecDeque;
use std::default::Default;
use std::mem;
use std::time::Instant;
// Everything above 16 MB is allocated exactly.
const MAX_BUFFER_SIZE_CLASS: u64 = 16 * 1024 * 1024;
// Number of seconds before unused memory is purged.
//
// TODO(pcwalton): jemalloc uses a sigmoidal decay curve here. Consider something similar.
const DECAY_TIME: f32 = 0.250;
// Number of seconds before we can reuse an object buffer.
//
// This helps avoid stalls. This is admittedly a bit of a hack.
const REUSE_TIME: f32 = 0.015;
pub struct GPUMemoryAllocator<D> where D: Device {
buffers_in_use: FxHashMap<BufferID, BufferAllocation<D>>,
textures_in_use: FxHashMap<TextureID, TextureAllocation<D>>,
framebuffers_in_use: FxHashMap<FramebufferID, FramebufferAllocation<D>>,
free_objects: VecDeque<FreeObject<D>>,
next_buffer_id: BufferID,
next_texture_id: TextureID,
next_framebuffer_id: FramebufferID,
bytes_committed: u64,
bytes_allocated: u64,
}
struct BufferAllocation<D> where D: Device {
buffer: D::Buffer,
size: u64,
tag: BufferTag,
}
struct TextureAllocation<D> where D: Device {
texture: D::Texture,
descriptor: TextureDescriptor,
tag: TextureTag,
}
struct FramebufferAllocation<D> where D: Device {
framebuffer: D::Framebuffer,
descriptor: TextureDescriptor,
tag: FramebufferTag,
}
struct FreeObject<D> where D: Device {
timestamp: Instant,
kind: FreeObjectKind<D>,
}
enum FreeObjectKind<D> where D: Device {
Buffer { id: BufferID, allocation: BufferAllocation<D> },
Texture { id: TextureID, allocation: TextureAllocation<D> },
Framebuffer { id: FramebufferID, allocation: FramebufferAllocation<D> },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TextureDescriptor {
width: u32,
height: u32,
format: TextureFormat,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BufferID(pub u64);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TextureID(pub u64);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FramebufferID(pub u64);
// For debugging and profiling.
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct BufferTag(pub &'static str);
// For debugging and profiling.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TextureTag(pub &'static str);
// For debugging and profiling.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FramebufferTag(pub &'static str);
impl<D> GPUMemoryAllocator<D> where D: Device {
pub fn new() -> GPUMemoryAllocator<D> {
GPUMemoryAllocator {
buffers_in_use: FxHashMap::default(),
textures_in_use: FxHashMap::default(),
framebuffers_in_use: FxHashMap::default(),
free_objects: VecDeque::new(),
next_buffer_id: BufferID(0),
next_texture_id: TextureID(0),
next_framebuffer_id: FramebufferID(0),
bytes_committed: 0,
bytes_allocated: 0,
}
}
pub fn allocate_buffer<T>(&mut self, device: &D, size: u64, tag: BufferTag) -> BufferID {
let mut byte_size = size * mem::size_of::<T>() as u64;
if byte_size < MAX_BUFFER_SIZE_CLASS {
byte_size = byte_size.next_power_of_two();
}
let now = Instant::now();
for free_object_index in 0..self.free_objects.len() {
match self.free_objects[free_object_index] {
FreeObject {
ref timestamp,
kind: FreeObjectKind::Buffer { ref allocation, .. },
} if allocation.size == byte_size &&
(now - *timestamp).as_secs_f32() >= REUSE_TIME => {}
_ => continue,
}
let (id, mut allocation) = match self.free_objects.remove(free_object_index) {
Some(FreeObject { kind: FreeObjectKind::Buffer { id, allocation }, .. }) => {
(id, allocation)
}
_ => unreachable!(),
};
allocation.tag = tag;
self.bytes_committed += allocation.size;
self.buffers_in_use.insert(id, allocation);
return id;
}
let buffer = device.create_buffer(BufferUploadMode::Dynamic);
device.allocate_buffer::<u8>(&buffer,
BufferData::Uninitialized(byte_size as usize),
BufferTarget::Vertex);
let id = self.next_buffer_id;
self.next_buffer_id.0 += 1;
debug!("mapping buffer: {:?} {} ({}x{}) {:?}",
id,
byte_size,
size,
mem::size_of::<T>(),
tag);
self.buffers_in_use.insert(id, BufferAllocation { buffer, size: byte_size, tag });
self.bytes_allocated += byte_size;
self.bytes_committed += byte_size;
id
}
pub fn allocate_texture(&mut self,
device: &D,
size: Vector2I,
format: TextureFormat,
tag: TextureTag)
-> TextureID {
let descriptor = TextureDescriptor {
width: size.x() as u32,
height: size.y() as u32,
format,
};
let byte_size = descriptor.byte_size();
for free_object_index in 0..self.free_objects.len() {
match self.free_objects[free_object_index] {
FreeObject { kind: FreeObjectKind::Texture { ref allocation, .. }, .. } if
allocation.descriptor == descriptor => {}
_ => continue,
}
let (id, mut allocation) = match self.free_objects.remove(free_object_index) {
Some(FreeObject { kind: FreeObjectKind::Texture { id, allocation }, .. }) => {
(id, allocation)
}
_ => unreachable!(),
};
allocation.tag = tag;
self.bytes_committed += allocation.descriptor.byte_size();
self.textures_in_use.insert(id, allocation);
return id;
}
debug!("mapping texture: {:?} {:?}", descriptor, tag);
let texture = device.create_texture(format, size);
let id = self.next_texture_id;
self.next_texture_id.0 += 1;
self.textures_in_use.insert(id, TextureAllocation { texture, descriptor, tag });
self.bytes_allocated += byte_size;
self.bytes_committed += byte_size;
id
}
pub fn allocate_framebuffer(&mut self,
device: &D,
size: Vector2I,
format: TextureFormat,
tag: FramebufferTag)
-> FramebufferID {
let descriptor = TextureDescriptor {
width: size.x() as u32,
height: size.y() as u32,
format,
};
let byte_size = descriptor.byte_size();
for free_object_index in 0..self.free_objects.len() {
match self.free_objects[free_object_index].kind {
FreeObjectKind::Framebuffer { ref allocation, .. } if allocation.descriptor ==
descriptor => {}
_ => continue,
}
let (id, mut allocation) = match self.free_objects.remove(free_object_index) {
Some(FreeObject { kind: FreeObjectKind::Framebuffer { id, allocation }, .. }) => {
(id, allocation)
}
_ => unreachable!(),
};
allocation.tag = tag;
self.bytes_committed += allocation.descriptor.byte_size();
self.framebuffers_in_use.insert(id, allocation);
return id;
}
debug!("mapping framebuffer: {:?} {:?}", descriptor, tag);
let texture = device.create_texture(format, size);
let framebuffer = device.create_framebuffer(texture);
let id = self.next_framebuffer_id;
self.next_framebuffer_id.0 += 1;
self.framebuffers_in_use.insert(id, FramebufferAllocation {
framebuffer,
descriptor,
tag,
});
self.bytes_allocated += byte_size;
self.bytes_committed += byte_size;
id
}
pub fn purge_if_needed(&mut self) {
let now = Instant::now();
loop {
match self.free_objects.front() {
Some(FreeObject { timestamp, .. }) if (now - *timestamp).as_secs_f32() >=
DECAY_TIME => {}
_ => break,
}
match self.free_objects.pop_front() {
None => break,
Some(FreeObject { kind: FreeObjectKind::Buffer { allocation, .. }, .. }) => {
debug!("purging buffer: {}", allocation.size);
self.bytes_allocated -= allocation.size;
}
Some(FreeObject { kind: FreeObjectKind::Texture { allocation, .. }, .. }) => {
debug!("purging texture: {:?}", allocation.descriptor);
self.bytes_allocated -= allocation.descriptor.byte_size();
}
Some(FreeObject { kind: FreeObjectKind::Framebuffer { allocation, .. }, .. }) => {
debug!("purging framebuffer: {:?}", allocation.descriptor);
self.bytes_allocated -= allocation.descriptor.byte_size();
}
}
}
}
pub fn free_buffer(&mut self, id: BufferID) {
let allocation = self.buffers_in_use
.remove(&id)
.expect("Attempted to free unallocated buffer!");
self.bytes_committed -= allocation.size;
self.free_objects.push_back(FreeObject {
timestamp: Instant::now(),
kind: FreeObjectKind::Buffer { id, allocation },
});
}
pub fn free_texture(&mut self, id: TextureID) {
let allocation = self.textures_in_use
.remove(&id)
.expect("Attempted to free unallocated texture!");
let byte_size = allocation.descriptor.byte_size();
self.bytes_committed -= byte_size;
self.free_objects.push_back(FreeObject {
timestamp: Instant::now(),
kind: FreeObjectKind::Texture { id, allocation },
});
}
pub fn free_framebuffer(&mut self, id: FramebufferID) {
let allocation = self.framebuffers_in_use
.remove(&id)
.expect("Attempted to free unallocated framebuffer!");
let byte_size = allocation.descriptor.byte_size();
self.bytes_committed -= byte_size;
self.free_objects.push_back(FreeObject {
timestamp: Instant::now(),
kind: FreeObjectKind::Framebuffer { id, allocation },
});
}
pub fn get_buffer(&self, id: BufferID) -> &D::Buffer {
&self.buffers_in_use[&id].buffer
}
pub fn get_texture(&self, id: TextureID) -> &D::Texture {
&self.textures_in_use[&id].texture
}
pub fn get_framebuffer(&self, id: FramebufferID) -> &D::Framebuffer {
&self.framebuffers_in_use[&id].framebuffer
}
#[inline]
pub fn bytes_allocated(&self) -> u64 {
self.bytes_allocated
}
#[inline]
pub fn bytes_committed(&self) -> u64 {
self.bytes_committed
}
#[allow(dead_code)]
pub fn dump(&self) {
println!("GPU memory dump");
println!("---------------");
println!("Buffers:");
let mut ids: Vec<BufferID> = self.buffers_in_use.keys().cloned().collect();
ids.sort();
for id in ids {
let allocation = &self.buffers_in_use[&id];
println!("id {:?}: {:?} ({:?} B)", id, allocation.tag, allocation.size);
}
println!("Textures:");
let mut ids: Vec<TextureID> = self.textures_in_use.keys().cloned().collect();
ids.sort();
for id in ids {
let allocation = &self.textures_in_use[&id];
println!("id {:?}: {:?} {:?}x{:?} {:?} ({:?} B)",
id,
allocation.tag,
allocation.descriptor.width,
allocation.descriptor.height,
allocation.descriptor.format,
allocation.descriptor.byte_size());
}
println!("Framebuffers:");
let mut ids: Vec<FramebufferID> = self.framebuffers_in_use.keys().cloned().collect();
ids.sort();
for id in ids {
let allocation = &self.framebuffers_in_use[&id];
println!("id {:?}: {:?} {:?}x{:?} {:?} ({:?} B)",
id,
allocation.tag,
allocation.descriptor.width,
allocation.descriptor.height,
allocation.descriptor.format,
allocation.descriptor.byte_size());
}
}
}
impl TextureDescriptor {
fn byte_size(&self) -> u64 {
self.width as u64 * self.height as u64 * self.format.bytes_per_pixel() as u64
}
}

View File

@ -12,6 +12,10 @@
#[macro_use]
extern crate bitflags;
#[macro_use]
extern crate log;
pub mod allocator;
use half::f16;
use image::ImageFormat;
@ -21,11 +25,13 @@ use pathfinder_geometry::transform3d::Transform4F;
use pathfinder_geometry::vector::{Vector2I, vec2i};
use pathfinder_resources::ResourceLoader;
use pathfinder_simd::default::{F32x2, F32x4, I32x2};
use std::ops::Range;
use std::os::raw::c_void;
use std::time::Duration;
pub trait Device: Sized {
type Buffer;
type BufferDataReceiver;
type Fence;
type Framebuffer;
type ImageParameter;
@ -40,6 +46,8 @@ pub trait Device: Sized {
type VertexArray;
type VertexAttr;
fn backend_name(&self) -> &'static str;
fn device_name(&self) -> String;
fn feature_level(&self) -> FeatureLevel;
fn create_texture(&self, format: TextureFormat, size: Vector2I) -> Self::Texture;
fn create_texture_from_data(&self, format: TextureFormat, size: Vector2I, data: TextureDataRef)
@ -90,6 +98,8 @@ pub trait Device: Sized {
fn upload_to_texture(&self, texture: &Self::Texture, rect: RectI, data: TextureDataRef);
fn read_pixels(&self, target: &RenderTarget<Self>, viewport: RectI)
-> Self::TextureDataReceiver;
fn read_buffer(&self, buffer: &Self::Buffer, target: BufferTarget, range: Range<usize>)
-> Self::BufferDataReceiver;
fn begin_commands(&self);
fn end_commands(&self);
fn draw_arrays(&self, index_count: u32, render_state: &RenderState<Self>);
@ -108,6 +118,8 @@ pub trait Device: Sized {
fn recv_timer_query(&self, query: &Self::TimerQuery) -> Duration;
fn try_recv_texture_data(&self, receiver: &Self::TextureDataReceiver) -> Option<TextureData>;
fn recv_texture_data(&self, receiver: &Self::TextureDataReceiver) -> TextureData;
fn try_recv_buffer(&self, receiver: &Self::BufferDataReceiver) -> Option<Vec<u8>>;
fn recv_buffer(&self, receiver: &Self::BufferDataReceiver) -> Vec<u8>;
fn create_texture_from_png(&self,
resources: &dyn ResourceLoader,
@ -131,6 +143,30 @@ pub trait Device: Sized {
}
}
fn upload_png_to_texture(&self,
resources: &dyn ResourceLoader,
name: &str,
texture: &Self::Texture,
format: TextureFormat) {
let data = resources.slurp(&format!("textures/{}.png", name)).unwrap();
let image = image::load_from_memory_with_format(&data, ImageFormat::Png).unwrap();
match format {
TextureFormat::R8 => {
let image = image.to_luma();
let size = vec2i(image.width() as i32, image.height() as i32);
let rect = RectI::new(Vector2I::default(), size);
self.upload_to_texture(&texture, rect, TextureDataRef::U8(&image))
}
TextureFormat::RGBA8 => {
let image = image.to_rgba();
let size = vec2i(image.width() as i32, image.height() as i32);
let rect = RectI::new(Vector2I::default(), size);
self.upload_to_texture(&texture, rect, TextureDataRef::U8(&image))
}
_ => unimplemented!(),
}
}
fn create_program_from_shader_names(
&self,
resources: &dyn ResourceLoader,
@ -170,7 +206,7 @@ pub enum FeatureLevel {
D3D11,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TextureFormat {
R8,
R16F,
@ -182,10 +218,11 @@ pub enum TextureFormat {
#[derive(Clone, Copy, Debug)]
pub enum VertexAttrType {
F32,
I16,
I8,
U16,
I16,
I32,
U8,
U16,
}
#[derive(Clone, Copy, Debug)]
@ -258,6 +295,7 @@ pub struct RenderState<'a, D> where D: Device {
pub uniforms: &'a [UniformBinding<'a, D::Uniform>],
pub textures: &'a [TextureBinding<'a, D::TextureParameter, D::Texture>],
pub images: &'a [ImageBinding<'a, D::ImageParameter, D::Texture>],
pub storage_buffers: &'a [(&'a D::StorageBuffer, &'a D::Buffer)],
pub viewport: RectI,
pub options: RenderOptions,
}