Skip to main content

ADR-017: DirectX 12 Resource Management Architecture

Status

Accepted

Context

The DirectX 12 renderer implementation had several critical issues that needed to be addressed:

  1. Memory Leaks: The resize() method was using unsafe { std::mem::zeroed() } to reset COM objects, which prevented proper cleanup and caused memory leaks
  2. Manual Reference Counting: Direct use of COM interfaces required manual AddRef/Release management, leading to potential use-after-free bugs
  3. Runtime State Errors: GPU resource state transitions were tracked manually, allowing invalid transitions to compile and fail at runtime
  4. Error Handling: Extensive use of .unwrap() throughout the codebase made debugging difficult and could cause panics in production
  5. Resource Lifecycle: No centralized tracking of GPU resources made it difficult to manage memory usage and implement deferred deletion

These issues were causing stability problems and making the codebase difficult to maintain and extend.

Decision

We implemented a comprehensive resource management architecture with five key components:

1. COM Pointer Wrapper (ComPtr<T>)

  • Smart pointer wrapper that handles COM reference counting automatically
  • Implements Drop for automatic cleanup
  • Provides null-safe operations
  • Supports Deref/DerefMut for ergonomic access

2. RAII Guards

  • MappedResource: Automatically unmaps GPU buffers
  • CommandListGuard: Ensures command lists are properly closed
  • ScopeCleanup: Generic cleanup for any closure
  • FenceWaitGuard: Waits for GPU operations with timeout

3. Type-Safe Resource States

  • Phantom type parameters enforce valid state transitions at compile time
  • TypedTexture<State> prevents invalid GPU state transitions
  • State transitions are methods that consume self and return new type
  • Invalid transitions simply don't exist as methods

4. Centralized Resource Manager

  • Tracks all GPU resources with unique IDs
  • Implements deferred deletion based on GPU fence values
  • Provides memory usage statistics
  • Validates resource state

5. Builder Pattern

  • Dx12RendererBuilder provides fluent API for configuration
  • Validates configuration before construction
  • Enables debug features conditionally
  • Improves API discoverability

Consequences

Positive ✅

  • Memory Safety: Automatic cleanup prevents leaks and use-after-free bugs
  • Compile-Time Safety: Invalid GPU state transitions caught at compile time
  • Better Error Handling: All operations return Result<T> with descriptive errors
  • Improved Debugging: Clear ownership model and debug logging
  • Zero-Cost Abstractions: No runtime overhead compared to manual management
  • Future-Proof: Easier to add new features and maintain existing code
  • Better Developer Experience: RAII patterns reduce boilerplate

Negative ❌

  • Learning Curve: Developers need to understand the new patterns
  • Migration Effort: Existing code needs to be updated
  • Increased Compilation Time: More generic code and type checking
  • Verbosity: Some operations require more explicit type annotations

Alternatives Considered

1. Reference Counting with Arc<Mutex<T>>

  • Rejected: Too much overhead for single-threaded renderer
  • Would add unnecessary synchronization costs

2. Raw Pointer Management with Strict Conventions

  • Rejected: Still error-prone and difficult to enforce
  • Doesn't leverage Rust's type system

3. Using wgpu Instead of DirectX 12

  • Rejected: Performance requirements demand native DirectX 12
  • wgpu abstraction layer adds overhead

4. Runtime State Validation Only

  • Rejected: Compile-time guarantees are superior
  • Runtime checks add overhead and can still miss bugs

Implementation Details

Memory Leak Fix

// Before (leaks memory)
self.render_target = unsafe { std::mem::zeroed() };

// After (proper cleanup)
self.render_target = None; // Drop called on old value

Type-Safe Transitions

// Invalid transition won't compile
let rt: TypedTexture&lt;RenderTarget&gt; = create_render_target();
// let bad = rt.to_copy_dest(&cmd_list); // No such method!

// Valid transition compiles
let sr: TypedTexture&lt;ShaderResource&gt; = rt.to_shader_resource(&cmd_list);

RAII in Action

{
let mapped = MappedResource::new(&buffer, 0, None)?;
write_data(mapped.data());
} // Automatically unmapped here

Migration Strategy

  1. Wrap all COM objects in ComPtr&lt;T&gt;
  2. Replace manual state tracking with TypedTexture&lt;State&gt;
  3. Replace .unwrap() with proper error propagation
  4. Update creation to use builder pattern
  5. Add resources to centralized manager

See dx12-renderer-v2.md for detailed migration guide.

Date

2025-01-12

Authors

  • kittyn (via Claude Code)