diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6058960..83df4d9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,14 +7,18 @@ jobs: name: Check MSRV (1.71.0) strategy: matrix: - include: + target: - os: ubuntu-latest - features: vulkan + backend: vulkan - os: windows-latest - features: vulkan,d3d12 + backend: vulkan,d3d12 - os: macos-latest - features: vulkan,metal - runs-on: ${{ matrix.os }} + backend: vulkan,metal + features: + - hashbrown + - std + - hashbrown,std + runs-on: ${{ matrix.target.os }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@nightly @@ -22,26 +26,29 @@ jobs: run: cargo +nightly generate-lockfile -Zminimal-versions - uses: dtolnay/rust-toolchain@1.71.0 # Note that examples are extempt from the MSRV check, so that they can use newer Rust features - - run: cargo check --workspace --features ${{ matrix.features }} --no-default-features + - run: cargo check --workspace --features ${{ matrix.target.backend }},${{ matrix.features }} --no-default-features test: name: Test Suite strategy: matrix: - include: + target: - os: ubuntu-latest - features: vulkan,visualizer + backend: vulkan - os: windows-latest - features: vulkan,visualizer,d3d12,public-winapi + backend: vulkan,d3d12 - os: macos-latest - features: vulkan,visualizer,metal - runs-on: ${{ matrix.os }} + backend: vulkan,metal + features: + - std + - hashbrown,std + runs-on: ${{ matrix.target.os }} steps: - uses: actions/checkout@v4 - name: Cargo test all targets - run: cargo test --workspace --all-targets --features ${{ matrix.features }} --no-default-features + run: cargo test --workspace --all-targets --features visualizer,${{ matrix.target.backend }},${{ matrix.features }} --no-default-features - name: Cargo test docs - run: cargo test --workspace --doc --features ${{ matrix.features }} --no-default-features + run: cargo test --workspace --doc --features visualizer,${{ matrix.target.backend }},${{ matrix.features }} --no-default-features fmt: name: Rustfmt @@ -55,18 +62,21 @@ jobs: name: Clippy strategy: matrix: - include: + target: - os: ubuntu-latest - features: vulkan,visualizer + backend: vulkan - os: windows-latest - features: vulkan,visualizer,d3d12,public-winapi + backend: vulkan,d3d12 - os: macos-latest - features: vulkan,visualizer,metal - runs-on: ${{ matrix.os }} + backend: vulkan,metal + features: + - std + - hashbrown,std + runs-on: ${{ matrix.target.os }} steps: - uses: actions/checkout@v4 - name: Cargo clippy - run: cargo clippy --workspace --all-targets --features ${{ matrix.features }} --no-default-features -- -D warnings + run: cargo clippy --workspace --all-targets --features visualizer,${{ matrix.target.backend }},${{ matrix.features }} --no-default-features -- -D warnings doc: name: Build documentation diff --git a/Cargo.toml b/Cargo.toml index fa05010d..df2c4ab9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,15 +23,16 @@ include = [ all-features = true [dependencies] -log = "0.4" -thiserror = "1.0" -presser = { version = "0.3" } +log = { version = "0.4", default-features = false } +thiserror = { version = "2.0", default-features = false } +presser = { version = "0.3", default-features = false } # Only needed for Vulkan. Disable all default features as good practice, # such as the ability to link/load a Vulkan library. ash = { version = "0.38", optional = true, default-features = false, features = ["debug"] } # Only needed for visualizer. egui = { version = ">=0.24, <=0.27", optional = true, default-features = false } egui_extras = { version = ">=0.24, <=0.27", optional = true, default-features = false } +hashbrown = { version = "0.15.2", optional = true } [target.'cfg(target_vendor = "apple")'.dependencies] objc2 = { version = "0.6", default-features = false, optional = true } @@ -98,11 +99,14 @@ name = "metal-buffer" required-features = ["metal"] [features] +std = ["presser/std"] visualizer = ["dep:egui", "dep:egui_extras"] vulkan = ["dep:ash"] d3d12 = ["dep:windows"] metal = ["dep:objc2", "dep:objc2-metal", "dep:objc2-foundation"] # Expose helper functionality for winapi types to interface with gpu-allocator, which is primarily windows-rs driven public-winapi = ["dep:winapi"] +# Enables the FreeListAllocator when `std` is not enabled by using the `hashbrown` crate +hashbrown = ["dep:hashbrown"] -default = ["d3d12", "vulkan", "metal"] +default = ["std", "d3d12", "vulkan", "metal"] diff --git a/README.md b/README.md index 79e2403b..84758b1a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ```toml [dependencies] -gpu-allocator = "0.27.0" +gpu-allocator = { version = "0.27.0" } ``` ![Visualizer](visualizer.png) @@ -142,6 +142,7 @@ let mut allocator = Allocator::new(&AllocatorCreateDesc { ``` ## Simple Metal allocation example + ```rust use gpu_allocator::metal::*; use gpu_allocator::MemoryLocation; @@ -167,9 +168,39 @@ drop(resource); allocator.free(&allocation).unwrap(); ``` +## `no_std` Support + +`no_std` support can be enabled by compiling with `--no-default-features` to +disable `std` support and `--features hashbrown` for `Hash` collections that are only +defined in `std` for internal usages in crate. For example: + +```toml +[dependencies] +gpu-allocator = { version = "0.27", default-features = false, features = ["hashbrown", "other features"] } +``` + +To support both `std` and `no_std` builds in project, you can use the following +in your `Cargo.toml`: + +```toml +[features] +default = ["std", "other features"] + +std = ["gpu-allocator/std"] +hashbrown = ["gpu-allocator/hashbrown"] +other_features = [] + +[dependencies] +gpu-allocator = { version = "0.27", default-features = false } +``` + ## Minimum Supported Rust Version -The MSRV for this crate and the `vulkan`, `d3d12` and `metal` features is Rust 1.71. Any other features such as the `visualizer` (with all the `egui` dependencies) may have a higher requirement and are not tested in our CI. +The MSRV for this crate and the `vulkan`, `d3d12` and `metal` features is Rust **1.71**. + +The `no_std` support requires Rust **1.81** or higher because `no_std` support of dependency `thiserror` requires `core::error::Error` which is stabilized in **1.81**. + +Any other features such as the `visualizer` (with all the `egui` dependencies) may have a higher requirement and are not tested in our CI. ## License diff --git a/README.tpl b/README.tpl index dd3fd225..7afd85ab 100644 --- a/README.tpl +++ b/README.tpl @@ -12,16 +12,46 @@ ```toml [dependencies] -gpu-allocator = "0.27.0" +gpu-allocator = { version = "0.27.0" } ``` ![Visualizer](visualizer.png) {{readme}} +## `no_std` Support + +`no_std` support can be enabled by compiling with `--no-default-features` to +disable `std` support and `--features hashbrown` for `Hash` collections that are only +defined in `std` for internal usages in crate. For example: + +```toml +[dependencies] +gpu-allocator = { version = "0.27", default-features = false, features = ["hashbrown", "other features"] } +``` + +To support both `std` and `no_std` builds in project, you can use the following +in your `Cargo.toml`: + +```toml +[features] +default = ["std", "other features"] + +std = ["gpu-allocator/std"] +hashbrown = ["gpu-allocator/hashbrown"] +other_features = [] + +[dependencies] +gpu-allocator = { version = "0.27", default-features = false } +``` + ## Minimum Supported Rust Version -The MSRV for this crate and the `vulkan`, `d3d12` and `metal` features is Rust 1.71. Any other features such as the `visualizer` (with all the `egui` dependencies) may have a higher requirement and are not tested in our CI. +The MSRV for this crate and the `vulkan`, `d3d12` and `metal` features is Rust **1.71**. + +The `no_std` support requires Rust **1.81** or higher because `no_std` support of dependency `thiserror` requires `core::error::Error` which is stabilized in **1.81**. + +Any other features such as the `visualizer` (with all the `egui` dependencies) may have a higher requirement and are not tested in our CI. ## License diff --git a/release.toml b/release.toml index 619bc93a..78eec971 100644 --- a/release.toml +++ b/release.toml @@ -6,6 +6,6 @@ sign-tag = true publish = false pre-release-replacements = [ - { file = "README.md", search = "gpu-allocator = .*", replace = "{{crate_name}} = \"{{version}}\"" }, - { file = "README.tpl", search = "gpu-allocator = .*", replace = "{{crate_name}} = \"{{version}}\"" }, + { file = "README.md", search = "gpu-allocator = { version = \".*\"", replace = "{{crate_name}} = { version = \"{{version}}\"" }, + { file = "README.tpl", search = "gpu-allocator = { version = \".*\"", replace = "{{crate_name}} = { version = \"{{version}}\"" }, ] diff --git a/src/allocator/dedicated_block_allocator/mod.rs b/src/allocator/dedicated_block_allocator/mod.rs index 746b4dcd..02bb13df 100644 --- a/src/allocator/dedicated_block_allocator/mod.rs +++ b/src/allocator/dedicated_block_allocator/mod.rs @@ -1,12 +1,19 @@ #![deny(unsafe_code, clippy::unwrap_used)] +#[cfg(feature = "std")] +use alloc::sync::Arc; +use alloc::{ + borrow::ToOwned, + string::{String, ToString}, + vec::Vec, +}; +#[cfg(feature = "std")] +use std::backtrace::Backtrace; + +use log::{log, Level}; #[cfg(feature = "visualizer")] pub(crate) mod visualizer; -use std::{backtrace::Backtrace, sync::Arc}; - -use log::{log, Level}; - use super::{AllocationReport, AllocationType, SubAllocator, SubAllocatorBase}; use crate::{AllocationError, Result}; @@ -16,6 +23,7 @@ pub(crate) struct DedicatedBlockAllocator { allocated: u64, /// Only used if [`crate::AllocatorDebugSettings::store_stack_traces`] is [`true`] name: Option, + #[cfg(feature = "std")] backtrace: Arc, } @@ -25,6 +33,7 @@ impl DedicatedBlockAllocator { size, allocated: 0, name: None, + #[cfg(feature = "std")] backtrace: Arc::new(Backtrace::disabled()), } } @@ -39,8 +48,8 @@ impl SubAllocator for DedicatedBlockAllocator { _allocation_type: AllocationType, _granularity: u64, name: &str, - backtrace: Arc, - ) -> Result<(u64, std::num::NonZeroU64)> { + #[cfg(feature = "std")] backtrace: Arc, + ) -> Result<(u64, core::num::NonZeroU64)> { if self.allocated != 0 { return Err(AllocationError::OutOfMemory); } @@ -53,15 +62,18 @@ impl SubAllocator for DedicatedBlockAllocator { self.allocated = size; self.name = Some(name.to_string()); - self.backtrace = backtrace; + #[cfg(feature = "std")] + { + self.backtrace = backtrace; + } #[allow(clippy::unwrap_used)] - let dummy_id = std::num::NonZeroU64::new(1).unwrap(); + let dummy_id = core::num::NonZeroU64::new(1).unwrap(); Ok((0, dummy_id)) } - fn free(&mut self, chunk_id: Option) -> Result<()> { - if chunk_id != std::num::NonZeroU64::new(1) { + fn free(&mut self, chunk_id: Option) -> Result<()> { + if chunk_id != core::num::NonZeroU64::new(1) { Err(AllocationError::Internal("Chunk ID must be 1.".into())) } else { self.allocated = 0; @@ -71,10 +83,10 @@ impl SubAllocator for DedicatedBlockAllocator { fn rename_allocation( &mut self, - chunk_id: Option, + chunk_id: Option, name: &str, ) -> Result<()> { - if chunk_id != std::num::NonZeroU64::new(1) { + if chunk_id != core::num::NonZeroU64::new(1) { Err(AllocationError::Internal("Chunk ID must be 1.".into())) } else { self.name = Some(name.into()); @@ -90,6 +102,20 @@ impl SubAllocator for DedicatedBlockAllocator { ) { let empty = "".to_string(); let name = self.name.as_ref().unwrap_or(&empty); + let backtrace_info; + #[cfg(feature = "std")] + { + // TODO: Allocation could be avoided here if https://github.com/rust-lang/rust/pull/139135 is merged and stabilized. + backtrace_info = format!( + ", + backtrace: {}", + self.backtrace + ) + } + #[cfg(not(feature = "std"))] + { + backtrace_info = "" + } log!( log_level, @@ -98,16 +124,14 @@ impl SubAllocator for DedicatedBlockAllocator { memory block: {} dedicated allocation: {{ size: 0x{:x}, - name: {}, - backtrace: {} + name: {}{backtrace_info} }} }}"#, memory_type_index, memory_block_index, self.size, name, - self.backtrace - ) + ); } fn report_allocations(&self) -> Vec { diff --git a/src/allocator/free_list_allocator/mod.rs b/src/allocator/free_list_allocator/mod.rs index d7cde2e8..db969d68 100644 --- a/src/allocator/free_list_allocator/mod.rs +++ b/src/allocator/free_list_allocator/mod.rs @@ -1,16 +1,23 @@ #![deny(unsafe_code, clippy::unwrap_used)] - -#[cfg(feature = "visualizer")] -pub(crate) mod visualizer; - -use std::{ - backtrace::Backtrace, - collections::{HashMap, HashSet}, - sync::Arc, +#[cfg(feature = "std")] +use alloc::sync::Arc; +use alloc::{ + borrow::ToOwned, + string::{String, ToString}, + vec::Vec, }; +#[cfg(feature = "std")] +use std::backtrace::Backtrace; +#[cfg(all(feature = "std", not(feature = "hashbrown")))] +use std::collections::{HashMap, HashSet}; +#[cfg(feature = "hashbrown")] +use hashbrown::{HashMap, HashSet}; use log::{log, Level}; +#[cfg(feature = "visualizer")] +pub(crate) mod visualizer; + use super::{AllocationReport, AllocationType, SubAllocator, SubAllocatorBase}; use crate::{AllocationError, Result}; @@ -26,15 +33,16 @@ fn align_up(val: u64, alignment: u64) -> u64 { #[derive(Debug)] pub(crate) struct MemoryChunk { - pub(crate) chunk_id: std::num::NonZeroU64, + pub(crate) chunk_id: core::num::NonZeroU64, pub(crate) size: u64, pub(crate) offset: u64, pub(crate) allocation_type: AllocationType, pub(crate) name: Option, /// Only used if [`crate::AllocatorDebugSettings::store_stack_traces`] is [`true`] + #[cfg(feature = "std")] pub(crate) backtrace: Arc, - next: Option, - prev: Option, + next: Option, + prev: Option, } #[derive(Debug)] @@ -42,8 +50,8 @@ pub(crate) struct FreeListAllocator { size: u64, allocated: u64, pub(crate) chunk_id_counter: u64, - pub(crate) chunks: HashMap, - free_chunks: HashSet, + pub(crate) chunks: HashMap, + free_chunks: HashSet, } /// Test if two suballocations will overlap the same page. @@ -68,7 +76,7 @@ fn has_granularity_conflict(type0: AllocationType, type1: AllocationType) -> boo impl FreeListAllocator { pub(crate) fn new(size: u64) -> Self { #[allow(clippy::unwrap_used)] - let initial_chunk_id = std::num::NonZeroU64::new(1).unwrap(); + let initial_chunk_id = core::num::NonZeroU64::new(1).unwrap(); let mut chunks = HashMap::default(); chunks.insert( @@ -79,6 +87,7 @@ impl FreeListAllocator { offset: 0, allocation_type: AllocationType::Free, name: None, + #[cfg(feature = "std")] backtrace: Arc::new(Backtrace::disabled()), prev: None, next: None, @@ -100,7 +109,7 @@ impl FreeListAllocator { } /// Generates a new unique chunk ID - fn get_new_chunk_id(&mut self) -> Result { + fn get_new_chunk_id(&mut self) -> Result { if self.chunk_id_counter == u64::MAX { // End of chunk id counter reached, no more allocations are possible. return Err(AllocationError::OutOfMemory); @@ -108,19 +117,19 @@ impl FreeListAllocator { let id = self.chunk_id_counter; self.chunk_id_counter += 1; - std::num::NonZeroU64::new(id).ok_or_else(|| { + core::num::NonZeroU64::new(id).ok_or_else(|| { AllocationError::Internal("New chunk id was 0, which is not allowed.".into()) }) } /// Finds the specified `chunk_id` in the list of free chunks and removes if from the list - fn remove_id_from_free_list(&mut self, chunk_id: std::num::NonZeroU64) { + fn remove_id_from_free_list(&mut self, chunk_id: core::num::NonZeroU64) { self.free_chunks.remove(&chunk_id); } /// Merges two adjacent chunks. Right chunk will be merged into the left chunk fn merge_free_chunks( &mut self, - chunk_left: std::num::NonZeroU64, - chunk_right: std::num::NonZeroU64, + chunk_left: core::num::NonZeroU64, + chunk_right: core::num::NonZeroU64, ) -> Result<()> { // Gather data from right chunk and remove it let (right_size, right_next) = { @@ -162,14 +171,14 @@ impl SubAllocator for FreeListAllocator { allocation_type: AllocationType, granularity: u64, name: &str, - backtrace: Arc, - ) -> Result<(u64, std::num::NonZeroU64)> { + #[cfg(feature = "std")] backtrace: Arc, + ) -> Result<(u64, core::num::NonZeroU64)> { let free_size = self.size - self.allocated; if size > free_size { return Err(AllocationError::OutOfMemory); } - let mut best_fit_id: Option = None; + let mut best_fit_id: Option = None; let mut best_offset = 0u64; let mut best_aligned_size = 0u64; let mut best_chunk_size = 0u64; @@ -249,6 +258,7 @@ impl SubAllocator for FreeListAllocator { offset: free_chunk.offset, allocation_type, name: Some(name.to_string()), + #[cfg(feature = "std")] backtrace, prev: free_chunk.prev, next: Some(first_fit_id), @@ -278,7 +288,10 @@ impl SubAllocator for FreeListAllocator { chunk.allocation_type = allocation_type; chunk.name = Some(name.to_string()); - chunk.backtrace = backtrace; + #[cfg(feature = "std")] + { + chunk.backtrace = backtrace; + } self.remove_id_from_free_list(first_fit_id); @@ -290,7 +303,7 @@ impl SubAllocator for FreeListAllocator { Ok((best_offset, chunk_id)) } - fn free(&mut self, chunk_id: Option) -> Result<()> { + fn free(&mut self, chunk_id: Option) -> Result<()> { let chunk_id = chunk_id .ok_or_else(|| AllocationError::Internal("Chunk ID must be a valid value.".into()))?; @@ -302,7 +315,10 @@ impl SubAllocator for FreeListAllocator { })?; chunk.allocation_type = AllocationType::Free; chunk.name = None; - chunk.backtrace = Arc::new(Backtrace::disabled()); + #[cfg(feature = "std")] + { + chunk.backtrace = Arc::new(Backtrace::disabled()); + } self.allocated -= chunk.size; @@ -327,7 +343,7 @@ impl SubAllocator for FreeListAllocator { fn rename_allocation( &mut self, - chunk_id: Option, + chunk_id: Option, name: &str, ) -> Result<()> { let chunk_id = chunk_id @@ -362,7 +378,20 @@ impl SubAllocator for FreeListAllocator { } let empty = "".to_string(); let name = chunk.name.as_ref().unwrap_or(&empty); - + let backtrace_info; + #[cfg(feature = "std")] + { + // TODO: Allocation could be avoided here if https://github.com/rust-lang/rust/pull/139135 is merged and stabilized. + backtrace_info = format!( + ", + backtrace: {}", + chunk.backtrace + ) + } + #[cfg(not(feature = "std"))] + { + backtrace_info = "" + } log!( log_level, r#"leak detected: {{ @@ -373,8 +402,7 @@ impl SubAllocator for FreeListAllocator { size: 0x{:x}, offset: 0x{:x}, allocation_type: {:?}, - name: {}, - backtrace: {} + name: {}{backtrace_info} }} }}"#, memory_type_index, @@ -384,7 +412,6 @@ impl SubAllocator for FreeListAllocator { chunk.offset, chunk.allocation_type, name, - chunk.backtrace ); } } diff --git a/src/allocator/mod.rs b/src/allocator/mod.rs index db330c5a..1ef17c97 100644 --- a/src/allocator/mod.rs +++ b/src/allocator/mod.rs @@ -1,4 +1,9 @@ -use std::{backtrace::Backtrace, fmt, ops::Range, sync::Arc}; +#[cfg(feature = "std")] +use alloc::sync::Arc; +use alloc::{fmt, string::String, vec::Vec}; +use core::ops::Range; +#[cfg(feature = "std")] +use std::backtrace::Backtrace; use log::*; @@ -79,7 +84,7 @@ impl fmt::Debug for AllocationReport { impl fmt::Debug for AllocatorReport { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut allocations = self.allocations.clone(); - allocations.sort_by_key(|alloc| std::cmp::Reverse(alloc.size)); + allocations.sort_by_key(|alloc| core::cmp::Reverse(alloc.size)); let max_num_allocations_to_print = f.precision().unwrap_or(usize::MAX); allocations.truncate(max_num_allocations_to_print); @@ -87,7 +92,7 @@ impl fmt::Debug for AllocatorReport { f.debug_struct("AllocatorReport") .field( "summary", - &std::format_args!( + &core::format_args!( "{} / {}", fmt_bytes(self.total_allocated_bytes), fmt_bytes(self.total_capacity_bytes) @@ -113,14 +118,14 @@ pub(crate) trait SubAllocator: SubAllocatorBase + fmt::Debug + Sync + Send { allocation_type: AllocationType, granularity: u64, name: &str, - backtrace: Arc, - ) -> Result<(u64, std::num::NonZeroU64)>; + #[cfg(feature = "std")] backtrace: Arc, + ) -> Result<(u64, core::num::NonZeroU64)>; - fn free(&mut self, chunk_id: Option) -> Result<()>; + fn free(&mut self, chunk_id: Option) -> Result<()>; fn rename_allocation( &mut self, - chunk_id: Option, + chunk_id: Option, name: &str, ) -> Result<()>; diff --git a/src/d3d12/mod.rs b/src/d3d12/mod.rs index 68bac2dc..7f9f1e50 100644 --- a/src/d3d12/mod.rs +++ b/src/d3d12/mod.rs @@ -1,10 +1,13 @@ -use std::{ - backtrace::Backtrace, +#[cfg(feature = "std")] +use alloc::sync::Arc; +use alloc::{boxed::Box, string::String, vec::Vec}; +use core::{ fmt, // TODO: Remove when bumping MSRV to 1.80 mem::size_of_val, - sync::Arc, }; +#[cfg(feature = "std")] +use std::backtrace::Backtrace; use log::{debug, warn, Level}; use windows::Win32::{ @@ -36,49 +39,49 @@ mod public_winapi { impl ToWinapi for ID3D12Resource { fn as_winapi(&self) -> *const winapi_d3d12::ID3D12Resource { - unsafe { std::mem::transmute_copy(self) } + unsafe { core::mem::transmute_copy(self) } } fn as_winapi_mut(&mut self) -> *mut winapi_d3d12::ID3D12Resource { - unsafe { std::mem::transmute_copy(self) } + unsafe { core::mem::transmute_copy(self) } } } impl ToWinapi for ID3D12Device { fn as_winapi(&self) -> *const winapi_d3d12::ID3D12Device { - unsafe { std::mem::transmute_copy(self) } + unsafe { core::mem::transmute_copy(self) } } fn as_winapi_mut(&mut self) -> *mut winapi_d3d12::ID3D12Device { - unsafe { std::mem::transmute_copy(self) } + unsafe { core::mem::transmute_copy(self) } } } impl ToWindows for *const winapi_d3d12::ID3D12Device { fn as_windows(&self) -> &ID3D12Device { - unsafe { std::mem::transmute(self) } + unsafe { core::mem::transmute(self) } } } impl ToWindows for *mut winapi_d3d12::ID3D12Device { fn as_windows(&self) -> &ID3D12Device { - unsafe { std::mem::transmute(self) } + unsafe { core::mem::transmute(self) } } } impl ToWindows for &mut winapi_d3d12::ID3D12Device { fn as_windows(&self) -> &ID3D12Device { - unsafe { std::mem::transmute(self) } + unsafe { core::mem::transmute(self) } } } impl ToWinapi for ID3D12Heap { fn as_winapi(&self) -> *const winapi_d3d12::ID3D12Heap { - unsafe { std::mem::transmute_copy(self) } + unsafe { core::mem::transmute_copy(self) } } fn as_winapi_mut(&mut self) -> *mut winapi_d3d12::ID3D12Heap { - unsafe { std::mem::transmute_copy(self) } + unsafe { core::mem::transmute_copy(self) } } } } @@ -206,10 +209,10 @@ impl<'a> AllocationCreateDesc<'a> { let device = device.as_windows(); // Raw structs are binary-compatible let desc = unsafe { - std::mem::transmute::<&winapi_d3d12::D3D12_RESOURCE_DESC, &D3D12_RESOURCE_DESC>(desc) + core::mem::transmute::<&winapi_d3d12::D3D12_RESOURCE_DESC, &D3D12_RESOURCE_DESC>(desc) }; let allocation_info = - unsafe { device.GetResourceAllocationInfo(0, std::slice::from_ref(desc)) }; + unsafe { device.GetResourceAllocationInfo(0, core::slice::from_ref(desc)) }; let resource_category: ResourceCategory = desc.into(); AllocationCreateDesc { @@ -232,7 +235,7 @@ impl<'a> AllocationCreateDesc<'a> { location: MemoryLocation, ) -> Self { let allocation_info = - unsafe { device.GetResourceAllocationInfo(0, std::slice::from_ref(desc)) }; + unsafe { device.GetResourceAllocationInfo(0, core::slice::from_ref(desc)) }; let resource_category: ResourceCategory = desc.into(); AllocationCreateDesc { @@ -257,7 +260,7 @@ pub enum ID3D12DeviceVersion { Device12(ID3D12Device12), } -impl std::ops::Deref for ID3D12DeviceVersion { +impl core::ops::Deref for ID3D12DeviceVersion { type Target = ID3D12Device; fn deref(&self) -> &Self::Target { @@ -322,7 +325,7 @@ pub struct CommittedAllocationStatistics { #[derive(Debug)] pub struct Allocation { - chunk_id: Option, + chunk_id: Option, offset: u64, size: u64, memory_block_index: usize, @@ -333,7 +336,7 @@ pub struct Allocation { } impl Allocation { - pub fn chunk_id(&self) -> Option { + pub fn chunk_id(&self) -> Option { self.chunk_id } @@ -442,7 +445,7 @@ impl MemoryType { &mut self, device: &ID3D12DeviceVersion, desc: &AllocationCreateDesc<'_>, - backtrace: Arc, + #[cfg(feature = "std")] backtrace: Arc, allocation_sizes: &AllocationSizes, ) -> Result { let allocation_type = AllocationType::Linear; @@ -485,6 +488,7 @@ impl MemoryType { allocation_type, 1, desc.name, + #[cfg(feature = "std")] backtrace, )?; @@ -508,6 +512,7 @@ impl MemoryType { allocation_type, 1, desc.name, + #[cfg(feature = "std")] backtrace.clone(), ); @@ -558,6 +563,7 @@ impl MemoryType { allocation_type, 1, desc.name, + #[cfg(feature = "std")] backtrace, ); let (offset, chunk_id) = match allocation { @@ -735,6 +741,7 @@ impl Allocator { let size = desc.size; let alignment = desc.alignment; + #[cfg(feature = "std")] let backtrace = Arc::new(if self.debug_settings.store_stack_traces { Backtrace::force_capture() } else { @@ -746,6 +753,7 @@ impl Allocator { "Allocating `{}` of {} bytes with an alignment of {}.", &desc.name, size, alignment ); + #[cfg(feature = "std")] if self.debug_settings.log_stack_traces { let backtrace = Backtrace::force_capture(); debug!("Allocation stack trace: {}", backtrace); @@ -771,13 +779,20 @@ impl Allocator { }) .ok_or(AllocationError::NoCompatibleMemoryTypeFound)?; - memory_type.allocate(&self.device, desc, backtrace, &self.allocation_sizes) + memory_type.allocate( + &self.device, + desc, + #[cfg(feature = "std")] + backtrace, + &self.allocation_sizes, + ) } pub fn free(&mut self, allocation: Allocation) -> Result<()> { if self.debug_settings.log_frees { let name = allocation.name.as_deref().unwrap_or(""); debug!("Freeing `{}`.", name); + #[cfg(feature = "std")] if self.debug_settings.log_stack_traces { let backtrace = Backtrace::force_capture(); debug!("Free stack trace: {}", backtrace); diff --git a/src/lib.rs b/src/lib.rs index b148ace0..5e5171b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -174,6 +174,7 @@ //! ``` //! //! # Simple Metal allocation example +//! //! ```no_run //! # #[cfg(feature = "metal")] //! # fn main() { @@ -212,6 +213,21 @@ //! # fn main() {} //! ``` #![deny(clippy::unimplemented, clippy::unwrap_used, clippy::ok_expect)] +#![warn( + clippy::alloc_instead_of_core, + clippy::std_instead_of_alloc, + clippy::std_instead_of_core +)] +#![cfg_attr(not(feature = "std"), no_std)] + +#[macro_use] +extern crate alloc; + +#[cfg(all(not(feature = "std"), feature = "visualizer"))] +compile_error!("Cannot enable `visualizer` feature in `no_std` environment."); + +#[cfg(not(any(feature = "std", feature = "hashbrown")))] +compile_error!("Either `std` or `hashbrown` feature must be enabled"); mod result; pub use result::*; @@ -245,6 +261,7 @@ pub enum MemoryLocation { GpuToCpu, } +#[non_exhaustive] #[derive(Copy, Clone, Debug)] pub struct AllocatorDebugSettings { /// Logs out debugging information about the various heaps the current device has on startup @@ -254,12 +271,14 @@ pub struct AllocatorDebugSettings { /// Stores a copy of the full backtrace for every allocation made, this makes it easier to debug leaks /// or other memory allocations, but storing stack traces has a RAM overhead so should be disabled /// in shipping applications. + #[cfg(feature = "std")] pub store_stack_traces: bool, /// Log out every allocation as it's being made with log level Debug, rather spammy so off by default pub log_allocations: bool, /// Log out every free that is being called with log level Debug, rather spammy so off by default pub log_frees: bool, /// Log out stack traces when either `log_allocations` or `log_frees` is enabled. + #[cfg(feature = "std")] pub log_stack_traces: bool, } @@ -268,9 +287,11 @@ impl Default for AllocatorDebugSettings { Self { log_memory_information: false, log_leaks_on_shutdown: true, + #[cfg(feature = "std")] store_stack_traces: false, log_allocations: false, log_frees: false, + #[cfg(feature = "std")] log_stack_traces: false, } } diff --git a/src/metal/mod.rs b/src/metal/mod.rs index e3b283b6..605e61b0 100644 --- a/src/metal/mod.rs +++ b/src/metal/mod.rs @@ -1,9 +1,8 @@ -use std::{backtrace::Backtrace, sync::Arc}; - -#[cfg(feature = "visualizer")] -mod visualizer; -#[cfg(feature = "visualizer")] -pub use visualizer::AllocatorVisualizer; +#[cfg(feature = "std")] +use alloc::sync::Arc; +use alloc::{boxed::Box, string::ToString, vec::Vec}; +#[cfg(feature = "std")] +use std::backtrace::Backtrace; use log::debug; use objc2::{rc::Retained, runtime::ProtocolObject}; @@ -13,6 +12,11 @@ use objc2_metal::{ MTLStorageMode, MTLTextureDescriptor, }; +#[cfg(feature = "visualizer")] +mod visualizer; +#[cfg(feature = "visualizer")] +pub use visualizer::AllocatorVisualizer; + use crate::{ allocator::{self, AllocatorReport, MemoryBlockReport}, AllocationError, AllocationSizes, AllocatorDebugSettings, MemoryLocation, Result, @@ -29,7 +33,7 @@ fn memory_location_to_metal(location: MemoryLocation) -> MTLResourceOptions { #[derive(Debug)] pub struct Allocation { - chunk_id: Option, + chunk_id: Option, offset: u64, size: u64, memory_block_index: usize, @@ -152,8 +156,8 @@ pub struct Allocator { allocation_sizes: AllocationSizes, } -impl std::fmt::Debug for Allocator { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for Allocator { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { self.generate_report().fmt(f) } } @@ -225,7 +229,7 @@ impl MemoryType { &mut self, device: &ProtocolObject, desc: &AllocationCreateDesc<'_>, - backtrace: Arc, + #[cfg(feature = "std")] backtrace: Arc, allocation_sizes: &AllocationSizes, ) -> Result { let allocation_type = allocator::AllocationType::Linear; @@ -268,6 +272,7 @@ impl MemoryType { allocation_type, 1, desc.name, + #[cfg(feature = "std")] backtrace, )?; @@ -291,6 +296,7 @@ impl MemoryType { allocation_type, 1, desc.name, + #[cfg(feature = "std")] backtrace.clone(), ); @@ -341,6 +347,7 @@ impl MemoryType { allocation_type, 1, desc.name, + #[cfg(feature = "std")] backtrace, ); let (offset, chunk_id) = match allocation { @@ -452,6 +459,7 @@ impl Allocator { let size = desc.size; let alignment = desc.alignment; + #[cfg(feature = "std")] let backtrace = Arc::new(if self.debug_settings.store_stack_traces { Backtrace::force_capture() } else { @@ -463,6 +471,7 @@ impl Allocator { "Allocating `{}` of {} bytes with an alignment of {}.", &desc.name, size, alignment ); + #[cfg(feature = "std")] if self.debug_settings.log_stack_traces { let backtrace = Backtrace::force_capture(); debug!("Allocation stack trace: {}", backtrace); @@ -484,13 +493,20 @@ impl Allocator { }) .ok_or(AllocationError::NoCompatibleMemoryTypeFound)?; - memory_type.allocate(&self.device, desc, backtrace, &self.allocation_sizes) + memory_type.allocate( + &self.device, + desc, + #[cfg(feature = "std")] + backtrace, + &self.allocation_sizes, + ) } pub fn free(&mut self, allocation: &Allocation) -> Result<()> { if self.debug_settings.log_frees { let name = allocation.name.as_deref().unwrap_or(""); debug!("Freeing `{}`.", name); + #[cfg(feature = "std")] if self.debug_settings.log_stack_traces { let backtrace = Backtrace::force_capture(); debug!("Free stack trace: {}", backtrace); diff --git a/src/result.rs b/src/result.rs index 8ccb98a7..50bfff33 100644 --- a/src/result.rs +++ b/src/result.rs @@ -1,3 +1,5 @@ +use alloc::string::String; + use thiserror::Error; #[derive(Error, Debug)] @@ -22,4 +24,4 @@ pub enum AllocationError { CastableFormatsRequiresAtLeastDevice12, } -pub type Result = ::std::result::Result; +pub type Result = ::core::result::Result; diff --git a/src/visualizer/allocation_reports.rs b/src/visualizer/allocation_reports.rs index c95b51ae..fb03e38c 100644 --- a/src/visualizer/allocation_reports.rs +++ b/src/visualizer/allocation_reports.rs @@ -92,7 +92,7 @@ pub(crate) fn render_allocation_reports_ui( (AllocationReportVisualizeSorting::None, _) => {} (AllocationReportVisualizeSorting::Idx, true) => allocations.sort_by_key(|(idx, _)| *idx), (AllocationReportVisualizeSorting::Idx, false) => { - allocations.sort_by_key(|(idx, _)| std::cmp::Reverse(*idx)) + allocations.sort_by_key(|(idx, _)| core::cmp::Reverse(*idx)) } (AllocationReportVisualizeSorting::Name, true) => { allocations.sort_by(|(_, alloc1), (_, alloc2)| alloc1.name.cmp(&alloc2.name)) @@ -104,7 +104,7 @@ pub(crate) fn render_allocation_reports_ui( allocations.sort_by_key(|(_, alloc)| alloc.size) } (AllocationReportVisualizeSorting::Size, false) => { - allocations.sort_by_key(|(_, alloc)| std::cmp::Reverse(alloc.size)) + allocations.sort_by_key(|(_, alloc)| core::cmp::Reverse(alloc.size)) } } diff --git a/src/visualizer/memory_chunks.rs b/src/visualizer/memory_chunks.rs index ffe2afed..66156000 100644 --- a/src/visualizer/memory_chunks.rs +++ b/src/visualizer/memory_chunks.rs @@ -80,7 +80,6 @@ pub(crate) fn render_memory_chunks_ui<'a>( if cursor_idx < data.len() { bytes_required = data[cursor_idx].size; } - continue; } let bytes_used = bytes_required.min(bytes_left); diff --git a/src/vulkan/mod.rs b/src/vulkan/mod.rs index 549222b2..d3dfb6d1 100644 --- a/src/vulkan/mod.rs +++ b/src/vulkan/mod.rs @@ -1,9 +1,15 @@ -#[cfg(feature = "visualizer")] -mod visualizer; -use std::{backtrace::Backtrace, fmt, marker::PhantomData, sync::Arc}; +#[cfg(feature = "std")] +use alloc::sync::Arc; +use alloc::{borrow::ToOwned, boxed::Box, string::ToString, vec::Vec}; +use core::{fmt, marker::PhantomData}; +#[cfg(feature = "std")] +use std::backtrace::Backtrace; use ash::vk; use log::{debug, Level}; + +#[cfg(feature = "visualizer")] +mod visualizer; #[cfg(feature = "visualizer")] pub use visualizer::AllocatorVisualizer; @@ -43,7 +49,7 @@ pub struct AllocationCreateDesc<'a> { /// mark the entire [`Allocation`] as such, instead relying on the compiler to /// auto-implement this or fail if fields are added that violate this constraint #[derive(Clone, Copy, Debug)] -pub(crate) struct SendSyncPtr(std::ptr::NonNull); +pub(crate) struct SendSyncPtr(core::ptr::NonNull); // Sending is fine because mapped_ptr does not change based on the thread we are in unsafe impl Send for SendSyncPtr {} // Sync is also okay because Sending &Allocation is safe: a mutable reference @@ -147,7 +153,7 @@ pub struct AllocatorCreateDesc { /// [\[1\]]: presser#motivation #[derive(Debug)] pub struct Allocation { - chunk_id: Option, + chunk_id: Option, offset: u64, size: u64, memory_block_index: usize, @@ -196,7 +202,7 @@ impl Allocation { }) } - pub fn chunk_id(&self) -> Option { + pub fn chunk_id(&self) -> Option { self.chunk_id } @@ -239,7 +245,7 @@ impl Allocation { /// Returns a valid mapped pointer if the memory is host visible, otherwise it will return None. /// The pointer already points to the exact memory region of the suballocation, so no offset needs to be applied. - pub fn mapped_ptr(&self) -> Option> { + pub fn mapped_ptr(&self) -> Option> { self.mapped_ptr.map(|SendSyncPtr(p)| p) } @@ -247,7 +253,7 @@ impl Allocation { /// The slice already references the exact memory region of the allocation, so no offset needs to be applied. pub fn mapped_slice(&self) -> Option<&[u8]> { self.mapped_ptr().map(|ptr| unsafe { - std::slice::from_raw_parts(ptr.cast().as_ptr(), self.size as usize) + core::slice::from_raw_parts(ptr.cast().as_ptr(), self.size as usize) }) } @@ -255,7 +261,7 @@ impl Allocation { /// The slice already references the exact memory region of the allocation, so no offset needs to be applied. pub fn mapped_slice_mut(&mut self) -> Option<&mut [u8]> { self.mapped_ptr().map(|ptr| unsafe { - std::slice::from_raw_parts_mut(ptr.cast().as_ptr(), self.size as usize) + core::slice::from_raw_parts_mut(ptr.cast().as_ptr(), self.size as usize) }) } @@ -404,7 +410,7 @@ impl MemoryBlock { AllocationError::FailedToMap(e.to_string()) }) .and_then(|p| { - std::ptr::NonNull::new(p).map(SendSyncPtr).ok_or_else(|| { + core::ptr::NonNull::new(p).map(SendSyncPtr).ok_or_else(|| { AllocationError::FailedToMap("Returned mapped pointer is null".to_owned()) }) }) @@ -456,7 +462,7 @@ impl MemoryType { device: &ash::Device, desc: &AllocationCreateDesc<'_>, granularity: u64, - backtrace: Arc, + #[cfg(feature = "std")] backtrace: Arc, allocation_sizes: &AllocationSizes, ) -> Result { let allocation_type = if desc.linear { @@ -518,6 +524,7 @@ impl MemoryType { allocation_type, granularity, desc.name, + #[cfg(feature = "std")] backtrace, )?; @@ -544,6 +551,7 @@ impl MemoryType { allocation_type, granularity, desc.name, + #[cfg(feature = "std")] backtrace.clone(), ); @@ -552,7 +560,7 @@ impl MemoryType { let mapped_ptr = if let Some(SendSyncPtr(mapped_ptr)) = mem_block.mapped_ptr { let offset_ptr = unsafe { mapped_ptr.as_ptr().add(offset as usize) }; - std::ptr::NonNull::new(offset_ptr).map(SendSyncPtr) + core::ptr::NonNull::new(offset_ptr).map(SendSyncPtr) } else { None }; @@ -608,6 +616,7 @@ impl MemoryType { allocation_type, granularity, desc.name, + #[cfg(feature = "std")] backtrace, ); let (offset, chunk_id) = match allocation { @@ -625,7 +634,7 @@ impl MemoryType { let mapped_ptr = if let Some(SendSyncPtr(mapped_ptr)) = mem_block.mapped_ptr { let offset_ptr = unsafe { mapped_ptr.as_ptr().add(offset as usize) }; - std::ptr::NonNull::new(offset_ptr).map(SendSyncPtr) + core::ptr::NonNull::new(offset_ptr).map(SendSyncPtr) } else { None }; @@ -769,6 +778,7 @@ impl Allocator { let size = desc.requirements.size; let alignment = desc.requirements.alignment; + #[cfg(feature = "std")] let backtrace = Arc::new(if self.debug_settings.store_stack_traces { Backtrace::force_capture() } else { @@ -780,6 +790,7 @@ impl Allocator { "Allocating `{}` of {} bytes with an alignment of {}.", &desc.name, size, alignment ); + #[cfg(feature = "std")] if self.debug_settings.log_stack_traces { let backtrace = Backtrace::force_capture(); debug!("Allocation stack trace: {}", backtrace); @@ -834,6 +845,7 @@ impl Allocator { &self.device, desc, self.buffer_image_granularity, + #[cfg(feature = "std")] backtrace.clone(), &self.allocation_sizes, ) @@ -856,6 +868,7 @@ impl Allocator { &self.device, desc, self.buffer_image_granularity, + #[cfg(feature = "std")] backtrace, &self.allocation_sizes, ) @@ -871,6 +884,7 @@ impl Allocator { if self.debug_settings.log_frees { let name = allocation.name.as_deref().unwrap_or(""); debug!("Freeing `{}`.", name); + #[cfg(feature = "std")] if self.debug_settings.log_stack_traces { let backtrace = Backtrace::force_capture(); debug!("Free stack trace: {}", backtrace);