Skip to content

[Feature] Default buffer properties to DEVICE_LOCAL for static geometry #104

@vmarcella

Description

@vmarcella

Overview

Buffer defaults bias toward CPU-visible memory (adds COPY_DST), even for static vertex buffers that are uploaded once and never modified. This results in suboptimal memory placement on discrete GPUs and unnecessary overhead. Static geometry should default to DEVICE_LOCAL memory, with CPU-visible properties reserved for buffers that require CPU updates (uniform streams, dynamic meshes).

Current State

Properties::default() returns CPU_VISIBLE:

// crates/lambda-rs/src/render/buffer.rs
impl Default for Properties {
  fn default() -> Self {
    Properties::CPU_VISIBLE
  }
}

Additionally, build_from_mesh hardcodes CPU_VISIBLE for vertex buffers created from meshes:

// crates/lambda-rs/src/render/buffer.rs
pub fn build_from_mesh(
  mesh: &Mesh,
  gpu: &Gpu,
) -> Result<Buffer, &'static str> {
  let builder = Self::new();
  return builder
    .with_length(std::mem::size_of_val(mesh.vertices()))
    .with_usage(Usage::VERTEX)
    .with_properties(Properties::CPU_VISIBLE)
    .with_buffer_type(BufferType::Vertex)
    .build(gpu, mesh.vertices().to_vec());
}

This means static meshes that are loaded once and never modified are placed in CPU-visible memory, which on discrete GPUs means system RAM accessed over PCIe rather than fast VRAM.

Scope

Goals:

  • Default Properties to DEVICE_LOCAL for optimal GPU performance
  • Update build_from_mesh to use DEVICE_LOCAL
  • Provide clear guidance on when to use CPU_VISIBLE vs DEVICE_LOCAL

Non-Goals:

  • Automatic memory tier selection based on update frequency
  • Staging buffer abstraction for uploads (future work)

Proposed API

// crates/lambda-rs/src/render/buffer.rs
impl Default for Properties {
  fn default() -> Self {
    Properties::DEVICE_LOCAL  // Changed from CPU_VISIBLE
  }
}

// build_from_mesh updated to use DEVICE_LOCAL
pub fn build_from_mesh(
  mesh: &Mesh,
  gpu: &Gpu,
) -> Result<Buffer, &'static str> {
  let builder = Self::new();
  return builder
    .with_length(std::mem::size_of_val(mesh.vertices()))
    .with_usage(Usage::VERTEX)
    .with_properties(Properties::DEVICE_LOCAL)  // Changed from CPU_VISIBLE
    .with_buffer_type(BufferType::Vertex)
    .build(gpu, mesh.vertices().to_vec());
}

Usage guidance:

// Static geometry (loaded once, never updated) - use default DEVICE_LOCAL
let static_mesh_buffer = BufferBuilder::new()
  .with_usage(Usage::VERTEX)
  .build(&gpu, vertices)?;

// Dynamic geometry (updated per-frame) - explicit CPU_VISIBLE
let dynamic_buffer = BufferBuilder::new()
  .with_usage(Usage::VERTEX)
  .with_properties(Properties::CPU_VISIBLE)
  .build(&gpu, initial_vertices)?;

// Uniform buffers (updated frequently) - explicit CPU_VISIBLE
let uniform_buffer = BufferBuilder::new()
  .with_usage(Usage::UNIFORM)
  .with_properties(Properties::CPU_VISIBLE)
  .build(&gpu, uniform_data)?;

Acceptance Criteria

  • Properties::default() returns DEVICE_LOCAL
  • build_from_mesh uses DEVICE_LOCAL
  • Documentation updated explaining DEVICE_LOCAL vs CPU_VISIBLE tradeoffs
  • Examples using uniform buffers explicitly specify CPU_VISIBLE
  • Migration guide for users relying on implicit CPU_VISIBLE
  • Unit tests updated to reflect new defaults

Affected Crates

lambda-rs

Notes

  • This is a breaking change for users who rely on implicit CPU_VISIBLE for buffers they update from the CPU
  • Migration: add explicit .with_properties(Properties::CPU_VISIBLE) for dynamic buffers
  • Future work: staging buffer helper for efficient DEVICE_LOCAL uploads

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions