# Copilot Instructions for shdrlib
## Project Overview
**shdrlib** is a three-tiered Vulkan shader compilation and rendering framework built in Rust. This is a **backend library** focused on shader compilation and rendering, not a game engine or windowing framework.
all documentation is stored in the /md/{tier} directory [
core/[
CORE_DEV.md
CORE_IMPLEMENTATION.md
]
ex/[
EX_DEV.md
EX_IMPLEMENTATION_ROADMAP.md
EX_ARCHITECTURE.md
EX_QUICK_DECISIONS.md
]
ez/
]
## Architecture & Design Philosophy
### Three-Tier System
This crate implements a strict three-tier abstraction model:
1. **CORE (Tier 0)**: Thin wrappers around `ash` (Vulkan) and `glslang`/`shaderc` (shader compilation)
- "The assembly language of the library"
- Minimal abstraction, maximum control
- **Does NOT manage lifetimes or resource dependencies**
- Objects are "dumb wrappers" with basic `Drop` cleanup
- **Unsafe to use alone** - can cause UB if misused
2. **EX (Tier 1 - Explicit)**: Ergonomic layer with explicit control
- `ShaderManager`: Owns objects and manages lifetimes correctly
- `RuntimeManager`: Handles rendering operations with Arc'd resources
- Explicit configuration with minimal magic
- Builder patterns for complex object construction
3. **EZ (Tier 2 - Easy)**: High-level abstraction with intelligent defaults
- Wraps both EX managers and CORE functions
- Minimal configuration required
- Sensible defaults using Rust's `Default` trait
- Optimized for rapid development
### Core Design Principles
#### Zero-Cost Abstractions
- All tiers compile down to identical machine code
- Heavy use of `#[inline]` for wrapper functions
- No runtime overhead compared to raw Vulkan usage
- Aggressive compiler optimizations leveraged
#### Sterile, Persistent, Contained Objects
- **Sterile**: No hidden state, side effects, or magic behavior
- **Persistent**: Consistent behavior across tiers with compile-time guarantees
- **Contained**: Clear boundaries and responsibilities
#### Tier Interoperability
- Users can seamlessly drop down to lower tiers
- EZ functions accept CORE objects
- EX managers work with CORE primitives
- No lock-in at any abstraction level
## Code Style & Conventions
### Naming Conventions
- **Modules**: `snake_case` (e.g., `shader_manager`, `pipeline_builder`)
- **Types**: `PascalCase` (e.g., `ShaderManager`, `RuntimeManager`)
- **Functions**: `snake_case` (e.g., `create_pipeline`, `find_memory_type`)
- **Constants**: `SCREAMING_SNAKE_CASE` (e.g., `DEFAULT_TIMEOUT`, `MAX_DESCRIPTOR_SETS`)
- **Tier prefixes**: Use `core::`, `ex::`, `ez::` module prefixes
### Error Handling
- Use `thiserror` for all error types
- Errors should be tier-specific:
- **CORE**: Thin wrappers around Vulkan errors (`VulkanError(vk::Result)`)
- **EX**: More context added (`ShaderManagerError`, `RuntimeManagerError`)
- **EZ**: User-friendly messages with recovery suggestions
- Include line numbers for shader compilation errors
- Use `Result<T, E>` for all fallible operations
- Never use `.unwrap()` or `.expect()` in library code (only in examples/tests)
### Ownership & Lifetime Management
#### CORE Tier Rules
- Objects are thin wrappers with basic `Drop` cleanup
- **Do NOT** enforce lifetime dependencies between objects
- **Do NOT** hold references to other CORE objects
- Each object destroys its own Vulkan handle in `Drop`
- Users/EX tier must ensure correct drop order manually
Example CORE pattern:
```rust
pub struct Pipeline {
pipeline: vk::Pipeline,
layout: PipelineLayout, // Pipeline OWNS layout (correct drop order)
bind_point: vk::PipelineBindPoint,
}
```
#### EX Tier Rules
- `ShaderManager` **OWNS** all CORE objects
- Fields declared in correct destruction order (Rust drops fields in declaration order)
- `RuntimeManager` uses `Arc<T>` for shared ownership
- Explicit lifetime management with clear ownership
Example EX pattern:
```rust
pub struct ShaderManager {
instance: core::Instance,
device: core::Device,
shaders: Vec<core::Shader>,
pipelines: Vec<core::Pipeline>,
// Drop order: pipelines → shaders → device → instance
}
```
#### EZ Tier Rules
- Automatic resource management
- Hide lifetime complexity from users
- Sensible defaults for all optional parameters
### Safety & Unsafe Code
#### When to use `unsafe`
- Only at Vulkan FFI boundaries
- Wrapping raw Vulkan function calls
- Interacting with raw pointers from Vulkan
#### Safety Requirements
- Every `unsafe` block MUST have a `// SAFETY:` comment explaining invariants
- Validate Vulkan handles are non-null (in debug builds)
- Never expose raw pointers in public APIs
- Use newtype patterns for Vulkan handles when it improves safety
Example:
```rust
pub fn create_device(&self) -> Result<Device, DeviceError> {
// SAFETY: instance handle is valid, create_info is properly initialized
let device = unsafe {
self.instance.create_device(physical_device, &create_info, None)
}?;
Ok(Device { device })
}
```
### Documentation Standards
#### Rustdoc Requirements
- Every public item MUST have documentation
- CORE tier docs MUST warn about footguns and lifetime issues
- Include usage examples for non-trivial functions
- Link to higher tier alternatives when relevant
- Document all error conditions
Example CORE documentation:
```rust
/// Creates a new shader from GLSL source code.
///
/// # Warnings
///
/// This function does NOT enforce that the device is still valid.
/// Dropping the device before the shader causes undefined behavior.
/// Use `ex::ShaderManager` for safe lifetime management.
///
/// # Errors
///
/// Returns `ShaderError::CompilationFailed` if GLSL compilation fails.
/// The error includes line numbers when available.
///
/// # Example
///
/// ```rust
/// let shader = Shader::from_glsl(&device, VERTEX_SHADER_SRC, ShaderStage::Vertex)?;
/// ```
pub fn from_glsl(device: &Device, source: &str, stage: ShaderStage) -> Result<Self, ShaderError>
```
### Shader Compilation Patterns
#### GLSL to SPIR-V Pipeline
1. GLSL source → compiler (shaderc or glslang)
2. SPIR-V bytecode generation
3. **OPTIONAL**: Reflection pass for descriptor layouts
4. SPIR-V validation (debug builds only)
5. Shader module creation
6. **SPIR-V dropped** - module is all you need
#### Do NOT Store SPIR-V
```rust
// ❌ BAD - wastes memory
pub struct Shader {
module: vk::ShaderModule,
spirv: Vec<u32>, // Don't store this!
}
// ✅ GOOD - minimal footprint
pub struct Shader {
module: vk::ShaderModule,
stage: vk::ShaderStageFlags,
entry_point: String,
}
```
#### Shader Reflection
- Perform reflection BEFORE creating shader module
- Extract descriptor set layouts, push constants, input/output variables
- Store only the extracted metadata, not the SPIR-V
### Command Buffer Patterns
#### Accept Raw Vulkan Handles
CommandBuffer methods should accept raw `vk::*` handles, not CORE wrappers:
```rust
// ✅ GOOD - avoids borrow checker hell
impl CommandBuffer {
pub fn bind_pipeline(&self, bind_point: vk::PipelineBindPoint, pipeline: vk::Pipeline);
pub fn bind_descriptor_sets(&self, layout: vk::PipelineLayout, sets: &[vk::DescriptorSet]);
}
// ❌ BAD - causes borrow checker issues
impl CommandBuffer {
pub fn bind_pipeline(&self, bind_point: vk::PipelineBindPoint, pipeline: &Pipeline);
}
```
### Memory Management
#### No Smart Allocator at CORE
- Just wrap `vkAllocateMemory` directly
- Provide helper functions, not allocator structs
- No tracking of allocation state
```rust
// ✅ GOOD - simple function
pub fn allocate_buffer(
device: &Device,
size: u64,
usage: vk::BufferUsageFlags,
properties: vk::MemoryPropertyFlags,
) -> Result<Buffer, MemoryError>
// ❌ BAD - implies state tracking
pub struct MemoryAllocator {
allocations: Vec<Allocation>,
}
```
#### Buffer & Image Patterns
- `Buffer` owns its memory
- `Image` **always** owns an `ImageView` (not optional)
- Memory is bound at creation time
### Descriptor Patterns
#### Clear Update API
Use a dedicated struct for descriptor updates:
```rust
pub struct DescriptorWrite {
pub set: vk::DescriptorSet,
pub binding: u32,
pub descriptor_type: vk::DescriptorType,
pub resource: DescriptorResource,
}
pub enum DescriptorResource {
Buffer { buffer: vk::Buffer, offset: u64, range: u64 },
Image { view: vk::ImageView, layout: vk::ImageLayout, sampler: Option<vk::Sampler> },
}
```
#### DescriptorSet Ownership
- `DescriptorPool` owns all allocated sets
- `DescriptorSet` has NO `Drop` impl (pool handles cleanup)
### Pipeline Patterns
#### Pipeline Owns Layout
```rust
pub struct Pipeline {
pipeline: vk::Pipeline,
layout: PipelineLayout, // Owned - correct drop order guaranteed
bind_point: vk::PipelineBindPoint,
}
```
This is pragmatic - pipelines and layouts are always used together, and this ensures correct destruction order.
### Render Pass Strategy
#### Prefer Dynamic Rendering
- Use Vulkan 1.3+ dynamic rendering by default
- Support traditional render passes for compatibility
- **Recommend dynamic rendering** in all documentation
```rust
// ✅ PREFERRED - dynamic rendering (Vulkan 1.3+)
command_buffer.begin_rendering(&vk::RenderingInfo {
render_area: ...,
color_attachments: &[...],
depth_attachment: ...,
});
// ⚠️ LEGACY - traditional render passes (still supported)
let render_pass = RenderPass::new(&device, ...)?;
let framebuffer = Framebuffer::new(&device, &render_pass, ...)?;
```
## Testing Standards
### Test Organization
- Unit tests in same file as implementation (using `#[cfg(test)] mod tests`)
- Integration tests in `tests/` directory
- Examples in `examples/` directory
### CORE Tier Testing
- Test that `Drop` properly cleans up (use validation layers)
- Test error paths explicitly
- **Expect crashes if used wrong** - that's the point of CORE
- Use `#[should_panic]` for UB tests (educational)
### EX Tier Testing
- Test safe lifetime management
- Test that managers prevent UB
- Test builder patterns
- Integration tests showing realistic usage
### EZ Tier Testing
- Test default behaviors
- Test common rendering scenarios
- Test error recovery
- End-to-end rendering tests
## Performance Guidelines
### Optimization Priorities
1. Zero allocations in hot paths (rendering loop)
2. Inline wrapper functions liberally with `#[inline]`
3. Use `repr(transparent)` or `repr(C)` for wrapper types
4. Benchmark against raw `ash` usage to ensure zero overhead
5. Profile shader compilation times
### Benchmarking
- Compare CORE wrapper overhead vs raw Vulkan
- Measure EX tier manager overhead (should be zero)
- Track shader compilation performance
- Use `criterion` for microbenchmarks
## Dependencies Management
### Required Dependencies
```toml
[dependencies]
ash = "0.38" # Vulkan bindings
thiserror = "1.0" # Error handling
shaderc = "0.8" # Shader compilation
spirv-reflect = "0.2" # SPIR-V reflection
```
### Optional Dependencies
- Consider `gpu-allocator` for EX tier (smarter memory management)
- Consider `winit` for examples (windowing - but NOT in the library itself)
### Do NOT Include
- ❌ Windowing libraries (users provide their own)
- ❌ Input handling
- ❌ Asset loading
- ❌ Scene graphs or game engine features
## Common Patterns
### Builder Pattern
Use builders for complex object construction:
```rust
pub struct PipelineBuilder {
vertex_shader: Option<ShaderStageInfo>,
fragment_shader: Option<ShaderStageInfo>,
viewport: Option<vk::Viewport>,
// ... ~20 pipeline states
}
impl PipelineBuilder {
pub fn vertex_shader(mut self, shader: &Shader, entry: &str) -> Self {
self.vertex_shader = Some(ShaderStageInfo { shader, entry });
self
}
pub fn build(self, device: &Device) -> Result<Pipeline, PipelineError> {
// Validate required fields
// Create pipeline
}
}
```
### RAII Pattern
Use Rust's `Drop` for automatic cleanup:
```rust
impl Drop for Device {
fn drop(&mut self) {
unsafe {
self.logical_device.destroy_device(None);
}
}
}
```
### Newtype Pattern
Use newtypes for type safety:
```rust
pub struct ShaderId(usize);
pub struct PipelineId(usize);
// Type system prevents mixing shader IDs with pipeline IDs
```
## What NOT to Do
### Anti-Patterns to Avoid
#### ❌ Don't Add Magic at CORE Level
```rust
// BAD - too much magic for CORE
pub fn create_vertex_buffer<T>(data: &[T]) -> Result<Buffer, MemoryError>;
// GOOD - just wrap the API
pub fn create_buffer(size: u64, usage: vk::BufferUsageFlags) -> Result<Buffer, MemoryError>;
```
#### ❌ Don't Store Unnecessary Data
```rust
// BAD - wastes memory
pub struct Shader {
spirv: Vec<u32>, // Drop after creating module!
}
```
#### ❌ Don't Mix Tier Responsibilities
```rust
// BAD - CORE managing lifetimes (that's EX's job)
pub struct Pipeline {
device: Arc<Device>, // No! CORE objects don't know about each other
}
```
#### ❌ Don't Use Default Values at CORE
```rust
// BAD - CORE should be explicit
pub fn create_buffer(size: u64) -> Result<Buffer, MemoryError> {
create_buffer_impl(size, DEFAULT_USAGE) // No defaults in CORE!
}
```
#### ❌ Don't Panic in Library Code
```rust
// BAD - never panic
let device = Device::new(...).expect("Failed to create device");
// GOOD - return Result
let device = Device::new(...)?;
```
## Version Control & Commits
### Commit Message Style
- Use conventional commits: `feat:`, `fix:`, `docs:`, `refactor:`, etc.
- Include tier in scope: `feat(core): add shader reflection`
- Reference issue numbers when applicable
### Branch Strategy
- `main`: stable, working code
- `dev`: active development
- Feature branches: `feature/tier-name-description`
## AI Assistant Guidelines
When helping with this codebase:
1. **Always respect the three-tier architecture** - don't blur the lines
2. **CORE tier code should be minimal** - resist the urge to add features
3. **Prioritize safety at EX/EZ tiers** - but not at CORE
4. **Document lifetime footguns clearly** - users need to know the risks
5. **Suggest dropping to lower tiers** when users need more control
6. **Reference DEVELOPMENT_PLAN.md and CORE_DEV.md** for design decisions
7. **Use rustdoc format** for all documentation
8. **Include safety comments** for all `unsafe` blocks
9. **Validate against Rust idioms** - this is idiomatic Rust, not C++ in Rust syntax
10. **Think zero-cost** - if a change adds overhead, it needs strong justification
## Special Considerations
### Validation Layers
- Auto-enable in debug builds: `#[cfg(debug_assertions)]`
- Allow user override in release builds
- Include debug messenger in CORE tier
### Windows vs Cross-Platform
- Primary development on Windows
- Use PowerShell-compatible commands
- Test on Linux/macOS when possible
- Platform-specific code behind `cfg` flags
### Rust Edition
- Using Rust 2024 edition (`edition = "2024"`)
- Leverage latest Rust features when stable
- Document MSRV (Minimum Supported Rust Version) when determined
---
## Quick Reference
### When to Use Each Tier
- **Use CORE** when:
- Building your own manager/framework
- Need absolute control
- Integrating with existing Vulkan code
- You know exactly what you're doing
- **Use EX** when:
- Need explicit control with safety guarantees
- Building a custom renderer
- Want ergonomic APIs without magic
- Need to mix and match components
- **Use EZ** when:
- Building an app/game quickly
- Want sensible defaults
- Don't want to think about Vulkan details
- Rapid prototyping
### Key Mantras
1. **CORE is just wrappers** - Think "ash + better types"
2. **CORE is unsafe to use alone** - You can easily create UB
3. **EX makes it safe** - ShaderManager owns everything correctly
4. **EZ makes it easy** - One-liners for common tasks
5. **Zero-cost abstractions** - All tiers compile to identical code
6. **Progressive disclosure** - Start simple, drop down when needed
7. **Backend focus** - No windowing, input, or game engine features
---
**Remember**: This is a Vulkan rendering library, not a game engine. Stay focused on shader compilation and rendering. Let users bring their own window/input handling.