Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add no_std support #265

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
fbc57a3
add `default-feature = false` to some deps, `std` feature and `hashbr…
CrazyboyQCD Feb 12, 2025
8f853b0
add `no_std` feature compile error
CrazyboyQCD Feb 12, 2025
e3ca7fb
remove `std::backtrace::Backtrace` in `no_std`
CrazyboyQCD Feb 12, 2025
eb9e04c
replace `std::*` with `core::*` or `alloc::*`
CrazyboyQCD Feb 12, 2025
48be63c
add clippy lints for `no_std` maintenance
CrazyboyQCD Feb 13, 2025
4d7b668
revert formatting of `Cargo.toml`
CrazyboyQCD Feb 13, 2025
3cee15f
fix typos in deps
CrazyboyQCD Feb 13, 2025
51e8723
add non_exhaustive for `AllocatorDebugSettings` for backward compatible
CrazyboyQCD Feb 13, 2025
a4d3463
reorder imports and mods
CrazyboyQCD Feb 13, 2025
d1caf98
format backtrace ahead by feature instead of duplicated log statements
CrazyboyQCD Feb 13, 2025
4c45a01
reorder mod in allocator module
CrazyboyQCD Feb 13, 2025
f2a6bfb
make `std` feature conflict with `hashbrown` in compile error and gat…
CrazyboyQCD Feb 13, 2025
d59fe26
restore trailing comma
CrazyboyQCD Feb 27, 2025
3e00364
remove empty line and reorder import
CrazyboyQCD Feb 27, 2025
457411a
simplify memory leak logging
CrazyboyQCD Feb 27, 2025
578ada0
prefer using `hashbrown`'s collections when `std` and `hashbrown` are…
CrazyboyQCD Feb 27, 2025
39abe59
update `CI` for `std` environment
CrazyboyQCD Feb 27, 2025
742a2ce
document the `hashbrown` feature in `Cargo.toml`
CrazyboyQCD Apr 1, 2025
694b6ff
update `CI` with `no_std`
CrazyboyQCD Apr 1, 2025
3e453ed
emit compile error when none of `std` and `hashbrown` is enabled
CrazyboyQCD Apr 1, 2025
541e814
CI: Simplify and complete `matrix` setup
MarijnS95 Apr 1, 2025
4123593
update missed `std` usages
CrazyboyQCD Apr 2, 2025
3d962a4
keep style of compile error the same
CrazyboyQCD Apr 2, 2025
66dbf08
add todo related with storing of `format_args!` outside `format!`
CrazyboyQCD Apr 2, 2025
10d0742
update missed `std` usages
CrazyboyQCD Apr 2, 2025
0ab0c88
fix lint
CrazyboyQCD Apr 2, 2025
e896fab
update `CI`
CrazyboyQCD Apr 2, 2025
8bdedba
fix lint
CrazyboyQCD Apr 2, 2025
c4da9ba
add `no_std` content in `README`
CrazyboyQCD Apr 3, 2025
d73de57
fix `CI`
CrazyboyQCD Apr 3, 2025
d27a627
Merge branch 'main' into no-std-support
CrazyboyQCD Apr 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 29 additions & 19 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -7,41 +7,48 @@ 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
- name: Generate lockfile with minimal dependency versions
run: cargo +nightly generate-lockfile -Zminimal-versions
- uses: dtolnay/[email protected]
# 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
12 changes: 8 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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 version above **1.81** beacuase `no_std` support of dependency `thiserror` requires `core::error::Error` which is stabalized 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

32 changes: 31 additions & 1 deletion README.tpl
Original file line number Diff line number Diff line change
@@ -19,9 +19,39 @@ gpu-allocator = "0.27.0"

{{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 version above **1.81** beacuase `no_std` support of dependency `thiserror` requires `core::error::Error` which is stabalized 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

56 changes: 40 additions & 16 deletions src/allocator/dedicated_block_allocator/mod.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
#[cfg(feature = "std")]
backtrace: Arc<Backtrace>,
}

@@ -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<Backtrace>,
) -> Result<(u64, std::num::NonZeroU64)> {
#[cfg(feature = "std")] backtrace: Arc<Backtrace>,
) -> 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<std::num::NonZeroU64>) -> Result<()> {
if chunk_id != std::num::NonZeroU64::new(1) {
fn free(&mut self, chunk_id: Option<core::num::NonZeroU64>) -> 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<std::num::NonZeroU64>,
chunk_id: Option<core::num::NonZeroU64>,
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<AllocationReport> {
87 changes: 57 additions & 30 deletions src/allocator/free_list_allocator/mod.rs
Original file line number Diff line number Diff line change
@@ -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,24 +33,25 @@ 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<String>,
/// Only used if [`crate::AllocatorDebugSettings::store_stack_traces`] is [`true`]
#[cfg(feature = "std")]
pub(crate) backtrace: Arc<Backtrace>,
next: Option<std::num::NonZeroU64>,
prev: Option<std::num::NonZeroU64>,
next: Option<core::num::NonZeroU64>,
prev: Option<core::num::NonZeroU64>,
}

#[derive(Debug)]
pub(crate) struct FreeListAllocator {
size: u64,
allocated: u64,
pub(crate) chunk_id_counter: u64,
pub(crate) chunks: HashMap<std::num::NonZeroU64, MemoryChunk>,
free_chunks: HashSet<std::num::NonZeroU64>,
pub(crate) chunks: HashMap<core::num::NonZeroU64, MemoryChunk>,
free_chunks: HashSet<core::num::NonZeroU64>,
}

/// 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,27 +109,27 @@ impl FreeListAllocator {
}

/// Generates a new unique chunk ID
fn get_new_chunk_id(&mut self) -> Result<std::num::NonZeroU64> {
fn get_new_chunk_id(&mut self) -> Result<core::num::NonZeroU64> {
if self.chunk_id_counter == u64::MAX {
// End of chunk id counter reached, no more allocations are possible.
return Err(AllocationError::OutOfMemory);
}

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<Backtrace>,
) -> Result<(u64, std::num::NonZeroU64)> {
#[cfg(feature = "std")] backtrace: Arc<Backtrace>,
) -> 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<std::num::NonZeroU64> = None;
let mut best_fit_id: Option<core::num::NonZeroU64> = 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<std::num::NonZeroU64>) -> Result<()> {
fn free(&mut self, chunk_id: Option<core::num::NonZeroU64>) -> 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<std::num::NonZeroU64>,
chunk_id: Option<core::num::NonZeroU64>,
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
);
}
}
19 changes: 12 additions & 7 deletions src/allocator/mod.rs
Original file line number Diff line number Diff line change
@@ -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,15 +84,15 @@ 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);

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<Backtrace>,
) -> Result<(u64, std::num::NonZeroU64)>;
#[cfg(feature = "std")] backtrace: Arc<Backtrace>,
) -> Result<(u64, core::num::NonZeroU64)>;

fn free(&mut self, chunk_id: Option<std::num::NonZeroU64>) -> Result<()>;
fn free(&mut self, chunk_id: Option<core::num::NonZeroU64>) -> Result<()>;

fn rename_allocation(
&mut self,
chunk_id: Option<std::num::NonZeroU64>,
chunk_id: Option<core::num::NonZeroU64>,
name: &str,
) -> Result<()>;

55 changes: 35 additions & 20 deletions src/d3d12/mod.rs
Original file line number Diff line number Diff line change
@@ -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<winapi_d3d12::ID3D12Resource> 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<winapi_d3d12::ID3D12Device> 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<ID3D12Device> for *const winapi_d3d12::ID3D12Device {
fn as_windows(&self) -> &ID3D12Device {
unsafe { std::mem::transmute(self) }
unsafe { core::mem::transmute(self) }
}
}

impl ToWindows<ID3D12Device> for *mut winapi_d3d12::ID3D12Device {
fn as_windows(&self) -> &ID3D12Device {
unsafe { std::mem::transmute(self) }
unsafe { core::mem::transmute(self) }
}
}

impl ToWindows<ID3D12Device> for &mut winapi_d3d12::ID3D12Device {
fn as_windows(&self) -> &ID3D12Device {
unsafe { std::mem::transmute(self) }
unsafe { core::mem::transmute(self) }
}
}

impl ToWinapi<winapi_d3d12::ID3D12Heap> 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<std::num::NonZeroU64>,
chunk_id: Option<core::num::NonZeroU64>,
offset: u64,
size: u64,
memory_block_index: usize,
@@ -333,7 +336,7 @@ pub struct Allocation {
}

impl Allocation {
pub fn chunk_id(&self) -> Option<std::num::NonZeroU64> {
pub fn chunk_id(&self) -> Option<core::num::NonZeroU64> {
self.chunk_id
}

@@ -442,7 +445,7 @@ impl MemoryType {
&mut self,
device: &ID3D12DeviceVersion,
desc: &AllocationCreateDesc<'_>,
backtrace: Arc<Backtrace>,
#[cfg(feature = "std")] backtrace: Arc<Backtrace>,
allocation_sizes: &AllocationSizes,
) -> Result<Allocation> {
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("<null>");
debug!("Freeing `{}`.", name);
#[cfg(feature = "std")]
if self.debug_settings.log_stack_traces {
let backtrace = Backtrace::force_capture();
debug!("Free stack trace: {}", backtrace);
21 changes: 21 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
38 changes: 27 additions & 11 deletions src/metal/mod.rs
Original file line number Diff line number Diff line change
@@ -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<std::num::NonZeroU64>,
chunk_id: Option<core::num::NonZeroU64>,
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<dyn MTLDevice>,
desc: &AllocationCreateDesc<'_>,
backtrace: Arc<Backtrace>,
#[cfg(feature = "std")] backtrace: Arc<Backtrace>,
allocation_sizes: &AllocationSizes,
) -> Result<Allocation> {
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("<null>");
debug!("Freeing `{}`.", name);
#[cfg(feature = "std")]
if self.debug_settings.log_stack_traces {
let backtrace = Backtrace::force_capture();
debug!("Free stack trace: {}", backtrace);
4 changes: 3 additions & 1 deletion src/result.rs
Original file line number Diff line number Diff line change
@@ -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<V, E = AllocationError> = ::std::result::Result<V, E>;
pub type Result<V, E = AllocationError> = ::core::result::Result<V, E>;
4 changes: 2 additions & 2 deletions src/visualizer/allocation_reports.rs
Original file line number Diff line number Diff line change
@@ -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))
}
}

1 change: 0 additions & 1 deletion src/visualizer/memory_chunks.rs
Original file line number Diff line number Diff line change
@@ -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);
40 changes: 27 additions & 13 deletions src/vulkan/mod.rs
Original file line number Diff line number Diff line change
@@ -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<std::ffi::c_void>);
pub(crate) struct SendSyncPtr(core::ptr::NonNull<core::ffi::c_void>);
// 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<std::num::NonZeroU64>,
chunk_id: Option<core::num::NonZeroU64>,
offset: u64,
size: u64,
memory_block_index: usize,
@@ -196,7 +202,7 @@ impl Allocation {
})
}

pub fn chunk_id(&self) -> Option<std::num::NonZeroU64> {
pub fn chunk_id(&self) -> Option<core::num::NonZeroU64> {
self.chunk_id
}

@@ -239,23 +245,23 @@ 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<std::ptr::NonNull<std::ffi::c_void>> {
pub fn mapped_ptr(&self) -> Option<core::ptr::NonNull<core::ffi::c_void>> {
self.mapped_ptr.map(|SendSyncPtr(p)| p)
}

/// Returns a valid mapped slice if the memory is host visible, otherwise it will return None.
/// 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)
})
}

/// Returns a valid mapped mutable slice if the memory is host visible, otherwise it will return None.
/// 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<Backtrace>,
#[cfg(feature = "std")] backtrace: Arc<Backtrace>,
allocation_sizes: &AllocationSizes,
) -> Result<Allocation> {
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("<null>");
debug!("Freeing `{}`.", name);
#[cfg(feature = "std")]
if self.debug_settings.log_stack_traces {
let backtrace = Backtrace::force_capture();
debug!("Free stack trace: {}", backtrace);